From 0215b697a5c2634ac6307bf7bf44eb8e2c79ff54 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 28 Feb 2024 19:13:17 +0530 Subject: [PATCH 001/308] chore: update issue date icons (#3826) --- web/components/issues/issue-detail/sidebar.tsx | 7 ++++--- .../issue-layouts/properties/all-properties.tsx | 4 +++- .../spreadsheet/columns/due-date-column.tsx | 2 ++ .../spreadsheet/columns/start-date-column.tsx | 2 ++ .../issues/peek-overview/properties.tsx | 16 +++++++++++++--- web/constants/spreadsheet.ts | 6 +++--- 6 files changed, 27 insertions(+), 10 deletions(-) diff --git a/web/components/issues/issue-detail/sidebar.tsx b/web/components/issues/issue-detail/sidebar.tsx index d3ac3c9fd..a65cb7f16 100644 --- a/web/components/issues/issue-detail/sidebar.tsx +++ b/web/components/issues/issue-detail/sidebar.tsx @@ -11,7 +11,8 @@ import { XCircle, CircleDot, CopyPlus, - CalendarDays, + CalendarClock, + CalendarCheck2, } from "lucide-react"; // hooks import { useEstimate, useIssueDetail, useProject, useProjectState, useUser } from "hooks/store"; @@ -236,7 +237,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => {
- + Start date
= observer((props) => {
- + Due date
= observer((props) => { onChange={handleStartDate} maxDate={maxDate ?? undefined} placeholder="Start date" + icon={} buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"} disabled={isReadOnly} showTooltip @@ -301,6 +302,7 @@ export const IssueProperties: React.FC = observer((props) => { onChange={handleTargetDate} minDate={minDate ?? undefined} placeholder="Due date" + icon={} buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"} buttonClassName={shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group) ? "text-red-500" : ""} clearIconClassName="!text-custom-text-100" diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx index ebed73b76..e261797af 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx @@ -1,5 +1,6 @@ import React from "react"; import { observer } from "mobx-react-lite"; +import { CalendarCheck2 } from "lucide-react"; // hooks import { useProjectState } from "hooks/store"; // components @@ -43,6 +44,7 @@ export const SpreadsheetDueDateColumn: React.FC = observer((props: Props) }} disabled={disabled} placeholder="Due date" + icon={} buttonVariant="transparent-with-text" buttonContainerClassName="w-full" buttonClassName={cn("rounded-none text-left", { diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx index 82c00fc12..01f7fe793 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx @@ -1,5 +1,6 @@ import React from "react"; import { observer } from "mobx-react-lite"; +import { CalendarClock } from "lucide-react"; // components import { DateDropdown } from "components/dropdowns"; // helpers @@ -35,6 +36,7 @@ export const SpreadsheetStartDateColumn: React.FC = observer((props: Prop }} disabled={disabled} placeholder="Start date" + icon={} buttonVariant="transparent-with-text" buttonClassName="rounded-none text-left" buttonContainerClassName="w-full" diff --git a/web/components/issues/peek-overview/properties.tsx b/web/components/issues/peek-overview/properties.tsx index 2588dafec..2f5a02c11 100644 --- a/web/components/issues/peek-overview/properties.tsx +++ b/web/components/issues/peek-overview/properties.tsx @@ -1,6 +1,16 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; -import { Signal, Tag, Triangle, LayoutPanelTop, CircleDot, CopyPlus, XCircle, CalendarDays } from "lucide-react"; +import { + Signal, + Tag, + Triangle, + LayoutPanelTop, + CircleDot, + CopyPlus, + XCircle, + CalendarClock, + CalendarCheck2, +} from "lucide-react"; // hooks import { useIssueDetail, useProject, useProjectState } from "hooks/store"; // ui icons @@ -118,7 +128,7 @@ export const PeekOverviewProperties: FC = observer((pro {/* start date */}
- + Start date
= observer((pro {/* due date */}
- + Due date
Date: Wed, 28 Feb 2024 19:14:15 +0530 Subject: [PATCH 002/308] fix: usePage hook throws an error without projectId (#3827) --- web/hooks/store/use-page.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/web/hooks/store/use-page.ts b/web/hooks/store/use-page.ts index 8971acd22..2d4dbd5b1 100644 --- a/web/hooks/store/use-page.ts +++ b/web/hooks/store/use-page.ts @@ -9,13 +9,13 @@ export const usePage = (pageId: string) => { const { projectPageMap, projectArchivedPageMap } = context.projectPages; const { projectId, workspaceSlug } = context.app.router; - if (!projectId || !workspaceSlug) throw new Error("usePage must be used within ProjectProvider"); - - if (projectPageMap[projectId] && projectPageMap[projectId][pageId]) { - return projectPageMap[projectId][pageId]; - } else if (projectArchivedPageMap[projectId] && projectArchivedPageMap[projectId][pageId]) { - return projectArchivedPageMap[projectId][pageId]; - } else { + if (!projectId || !workspaceSlug) { + console.log("usePage must be used within ProjectProvider"); return; } + + if (projectPageMap[projectId] && projectPageMap[projectId][pageId]) return projectPageMap[projectId][pageId]; + else if (projectArchivedPageMap[projectId] && projectArchivedPageMap[projectId][pageId]) + return projectArchivedPageMap[projectId][pageId]; + else return; }; From 7abfbac4794b7899f1d01d041f7ab0e83ca916dc Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 28 Feb 2024 19:15:03 +0530 Subject: [PATCH 003/308] chore: spreadsheet layout dropdown z index improvement (#3825) --- web/components/issues/issue-layouts/spreadsheet/issue-row.tsx | 2 +- .../issues/issue-layouts/spreadsheet/spreadsheet-header.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx index 143c37fb3..9f4810c78 100644 --- a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx @@ -189,7 +189,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => { { const { displayProperties, displayFilters, handleDisplayFilterUpdate, isEstimateEnabled } = props; return ( - + From 51f795fbd76f7835eecfdcdc943314e993c1031a Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Wed, 28 Feb 2024 19:34:29 +0530 Subject: [PATCH 004/308] [WEB-477] feat: enhanced project issue filtering by cycles and modules (#3830) * feat: implemented cycle and module filter in project issues * feat: implemented cycle and module filter in draft and archived issues --- packages/types/src/view-props.d.ts | 4 + .../filters/applied-filters/cycle.tsx | 48 ++++++++++ .../filters/applied-filters/filters-list.tsx | 22 ++++- .../filters/applied-filters/index.ts | 2 + .../filters/applied-filters/module.tsx | 44 +++++++++ .../filters/header/filters/cycle.tsx | 96 +++++++++++++++++++ .../header/filters/filters-selection.tsx | 30 ++++++ .../filters/header/filters/index.ts | 2 + .../filters/header/filters/module.tsx | 89 +++++++++++++++++ .../filters/header/helpers/filter-option.tsx | 6 +- web/constants/issue.ts | 70 ++++++++++++-- web/store/issue/cycle/filter.store.ts | 2 + .../helpers/issue-filter-helper.store.ts | 4 + web/store/issue/module/filter.store.ts | 2 + 14 files changed, 411 insertions(+), 10 deletions(-) create mode 100644 web/components/issues/issue-layouts/filters/applied-filters/cycle.tsx create mode 100644 web/components/issues/issue-layouts/filters/applied-filters/module.tsx create mode 100644 web/components/issues/issue-layouts/filters/header/filters/cycle.tsx create mode 100644 web/components/issues/issue-layouts/filters/header/filters/module.tsx diff --git a/packages/types/src/view-props.d.ts b/packages/types/src/view-props.d.ts index b6454ae4c..8e11d9cea 100644 --- a/packages/types/src/view-props.d.ts +++ b/packages/types/src/view-props.d.ts @@ -60,6 +60,8 @@ export type TIssueParams = | "created_by" | "subscriber" | "labels" + | "cycle" + | "module" | "start_date" | "target_date" | "project" @@ -79,6 +81,8 @@ export interface IIssueFilterOptions { labels?: string[] | null; priority?: string[] | null; project?: string[] | null; + cycle?: string[] | null; + module?: string[] | null; start_date?: string[] | null; state?: string[] | null; state_group?: string[] | null; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/cycle.tsx b/web/components/issues/issue-layouts/filters/applied-filters/cycle.tsx new file mode 100644 index 000000000..6299bebd7 --- /dev/null +++ b/web/components/issues/issue-layouts/filters/applied-filters/cycle.tsx @@ -0,0 +1,48 @@ +import { observer } from "mobx-react-lite"; +import { X } from "lucide-react"; +// hooks +import { useCycle } from "hooks/store"; +// ui +import { CycleGroupIcon } from "@plane/ui"; +// types +import { TCycleGroups } from "@plane/types"; + +type Props = { + handleRemove: (val: string) => void; + values: string[]; + editable: boolean | undefined; +}; + +export const AppliedCycleFilters: React.FC = observer((props) => { + const { handleRemove, values, editable } = props; + // store hooks + const { getCycleById } = useCycle(); + + return ( + <> + {values.map((cycleId) => { + const cycleDetails = getCycleById(cycleId) ?? null; + + if (!cycleDetails) return null; + + const cycleStatus = (cycleDetails?.status ? cycleDetails?.status.toLocaleLowerCase() : "draft") as TCycleGroups; + + return ( +
+ + {cycleDetails.name} + {editable && ( + + )} +
+ ); + })} + + ); +}); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx b/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx index 4ca2538e5..03b0c5138 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx @@ -1,12 +1,15 @@ import { observer } from "mobx-react-lite"; import { X } from "lucide-react"; +import { useRouter } from "next/router"; // hooks -import { useUser } from "hooks/store"; +import { useApplication, useUser } from "hooks/store"; // components import { + AppliedCycleFilters, AppliedDateFilters, AppliedLabelsFilters, AppliedMembersFilters, + AppliedModuleFilters, AppliedPriorityFilters, AppliedProjectFilters, AppliedStateFilters, @@ -34,6 +37,9 @@ const dateFilters = ["start_date", "target_date"]; export const AppliedFiltersList: React.FC = observer((props) => { const { appliedFilters, handleClearAllFilters, handleRemoveFilter, labels, states, alwaysAllowEditing } = props; // store hooks + const { + router: { moduleId, cycleId }, + } = useApplication(); const { membership: { currentProjectRole }, } = useUser(); @@ -104,6 +110,20 @@ export const AppliedFiltersList: React.FC = observer((props) => { values={value} /> )} + {filterKey === "cycle" && !cycleId && ( + handleRemoveFilter("cycle", val)} + values={value} + /> + )} + {filterKey === "module" && !moduleId && ( + handleRemoveFilter("module", val)} + values={value} + /> + )} {isEditingAllowed && ( + )} +
+ ); + })} + + ); +}); diff --git a/web/components/issues/issue-layouts/filters/header/filters/cycle.tsx b/web/components/issues/issue-layouts/filters/header/filters/cycle.tsx new file mode 100644 index 000000000..47b3b0506 --- /dev/null +++ b/web/components/issues/issue-layouts/filters/header/filters/cycle.tsx @@ -0,0 +1,96 @@ +import React, { useState } from "react"; +import { observer } from "mobx-react"; +import sortBy from "lodash/sortBy"; +// components +import { FilterHeader, FilterOption } from "components/issues"; +import { useApplication, useCycle } from "hooks/store"; +// ui +import { Loader, CycleGroupIcon } from "@plane/ui"; +// types +import { TCycleGroups } from "@plane/types"; + +type Props = { + appliedFilters: string[] | null; + handleUpdate: (val: string) => void; + searchQuery: string; +}; + +export const FilterCycle: React.FC = observer((props) => { + const { appliedFilters, handleUpdate, searchQuery } = props; + + // hooks + const { + router: { projectId }, + } = useApplication(); + const { getCycleById, getProjectCycleIds } = useCycle(); + + // states + const [itemsToRender, setItemsToRender] = useState(5); + const [previewEnabled, setPreviewEnabled] = useState(true); + + const cycleIds = projectId ? getProjectCycleIds(projectId) : undefined; + const cycles = cycleIds?.map((projectId) => getCycleById(projectId)!) ?? null; + const appliedFiltersCount = appliedFilters?.length ?? 0; + const filteredOptions = sortBy( + cycles?.filter((cycle) => cycle.name.toLowerCase().includes(searchQuery.toLowerCase())), + (cycle) => cycle.name.toLowerCase() + ); + + const handleViewToggle = () => { + if (!filteredOptions) return; + + if (itemsToRender === filteredOptions.length) setItemsToRender(5); + else setItemsToRender(filteredOptions.length); + }; + + const cycleStatus = (status: TCycleGroups) => (status ? status.toLocaleLowerCase() : "draft") as TCycleGroups; + + return ( + <> + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={previewEnabled} + handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + <> + {filteredOptions.slice(0, itemsToRender).map((cycle) => ( + handleUpdate(cycle.id)} + icon={ + + } + title={cycle.name} + activePulse={cycleStatus(cycle?.status) === "current" ? true : false} + /> + ))} + {filteredOptions.length > 5 && ( + + )} + + ) : ( +

No matches found

+ ) + ) : ( + + + + + + )} +
+ )} + + ); +}); diff --git a/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx b/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx index af8cfc84a..afdee86f2 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx @@ -1,6 +1,8 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; import { Search, X } from "lucide-react"; +// hooks +import { useApplication } from "hooks/store"; // components import { FilterAssignees, @@ -13,6 +15,8 @@ import { FilterState, FilterStateGroup, FilterTargetDate, + FilterCycle, + FilterModule, } from "components/issues"; // types import { IIssueFilterOptions, IIssueLabel, IState } from "@plane/types"; @@ -30,6 +34,10 @@ type Props = { export const FilterSelection: React.FC = observer((props) => { const { filters, handleFiltersUpdate, layoutDisplayFiltersOptions, labels, memberIds, states } = props; + // hooks + const { + router: { moduleId, cycleId }, + } = useApplication(); // states const [filtersSearchQuery, setFiltersSearchQuery] = useState(""); @@ -102,6 +110,28 @@ export const FilterSelection: React.FC = observer((props) => {
)} + {/* cycle */} + {isFilterEnabled("cycle") && !cycleId && ( +
+ handleFiltersUpdate("cycle", val)} + searchQuery={filtersSearchQuery} + /> +
+ )} + + {/* module */} + {isFilterEnabled("module") && !moduleId && ( +
+ handleFiltersUpdate("module", val)} + searchQuery={filtersSearchQuery} + /> +
+ )} + {/* assignees */} {isFilterEnabled("mentions") && (
diff --git a/web/components/issues/issue-layouts/filters/header/filters/index.ts b/web/components/issues/issue-layouts/filters/header/filters/index.ts index 2d3a04d0f..ab5756bf4 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/index.ts +++ b/web/components/issues/issue-layouts/filters/header/filters/index.ts @@ -8,4 +8,6 @@ export * from "./project"; export * from "./start-date"; export * from "./state-group"; export * from "./state"; +export * from "./cycle"; +export * from "./module"; export * from "./target-date"; diff --git a/web/components/issues/issue-layouts/filters/header/filters/module.tsx b/web/components/issues/issue-layouts/filters/header/filters/module.tsx new file mode 100644 index 000000000..49e00f84d --- /dev/null +++ b/web/components/issues/issue-layouts/filters/header/filters/module.tsx @@ -0,0 +1,89 @@ +import React, { useState } from "react"; +import { observer } from "mobx-react"; +import sortBy from "lodash/sortBy"; +// components +import { FilterHeader, FilterOption } from "components/issues"; +import { useApplication, useModule } from "hooks/store"; +// ui +import { Loader, DiceIcon } from "@plane/ui"; + +type Props = { + appliedFilters: string[] | null; + handleUpdate: (val: string) => void; + searchQuery: string; +}; + +export const FilterModule: React.FC = observer((props) => { + const { appliedFilters, handleUpdate, searchQuery } = props; + + // hooks + const { + router: { projectId }, + } = useApplication(); + const { getModuleById, getProjectModuleIds } = useModule(); + + // states + const [itemsToRender, setItemsToRender] = useState(5); + const [previewEnabled, setPreviewEnabled] = useState(true); + + const moduleIds = projectId ? getProjectModuleIds(projectId) : undefined; + const modules = moduleIds?.map((projectId) => getModuleById(projectId)!) ?? null; + const appliedFiltersCount = appliedFilters?.length ?? 0; + const filteredOptions = sortBy( + modules?.filter((module) => module.name.toLowerCase().includes(searchQuery.toLowerCase())), + (module) => module.name.toLowerCase() + ); + + const handleViewToggle = () => { + if (!filteredOptions) return; + + if (itemsToRender === filteredOptions.length) setItemsToRender(5); + else setItemsToRender(filteredOptions.length); + }; + + return ( + <> + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={previewEnabled} + handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + <> + {filteredOptions.slice(0, itemsToRender).map((cycle) => ( + handleUpdate(cycle.id)} + icon={} + title={cycle.name} + /> + ))} + {filteredOptions.length > 5 && ( + + )} + + ) : ( +

No matches found

+ ) + ) : ( + + + + + + )} +
+ )} + + ); +}); diff --git a/web/components/issues/issue-layouts/filters/header/helpers/filter-option.tsx b/web/components/issues/issue-layouts/filters/header/helpers/filter-option.tsx index f46d962ea..26c7bfaf5 100644 --- a/web/components/issues/issue-layouts/filters/header/helpers/filter-option.tsx +++ b/web/components/issues/issue-layouts/filters/header/helpers/filter-option.tsx @@ -8,10 +8,11 @@ type Props = { title: React.ReactNode; onClick?: () => void; multiple?: boolean; + activePulse?: boolean; }; export const FilterOption: React.FC = (props) => { - const { icon, isChecked, multiple = true, onClick, title } = props; + const { icon, isChecked, multiple = true, onClick, title, activePulse = false } = props; return (
+ {activePulse && ( +
+ )} ); }; diff --git a/web/constants/issue.ts b/web/constants/issue.ts index b2a8cd855..f1bcc3a06 100644 --- a/web/constants/issue.ts +++ b/web/constants/issue.ts @@ -263,7 +263,17 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { }, archived_issues: { list: { - filters: ["priority", "state", "assignees", "created_by", "labels", "start_date", "target_date"], + filters: [ + "priority", + "state", + "cycle", + "module", + "assignees", + "created_by", + "labels", + "start_date", + "target_date", + ], display_properties: true, display_filters: { group_by: ["state", "state_detail.group", "priority", "labels", "assignees", "created_by", null], @@ -278,7 +288,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { }, draft_issues: { list: { - filters: ["priority", "state_group", "labels", "start_date", "target_date"], + filters: ["priority", "state_group", "cycle", "module", "labels", "start_date", "target_date"], display_properties: true, display_filters: { group_by: ["state_detail.group", "priority", "project", "labels", null], @@ -291,7 +301,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { }, }, kanban: { - filters: ["priority", "state_group", "labels", "start_date", "target_date"], + filters: ["priority", "state_group", "cycle", "module", "labels", "start_date", "target_date"], display_properties: true, display_filters: { group_by: ["state_detail.group", "priority", "project", "labels"], @@ -350,7 +360,18 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { }, issues: { list: { - filters: ["priority", "state", "assignees", "mentions", "created_by", "labels", "start_date", "target_date"], + filters: [ + "priority", + "state", + "cycle", + "module", + "assignees", + "mentions", + "created_by", + "labels", + "start_date", + "target_date", + ], display_properties: true, display_filters: { group_by: ["state", "priority", "labels", "assignees", "created_by", null], @@ -363,7 +384,18 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { }, }, kanban: { - filters: ["priority", "state", "assignees", "mentions", "created_by", "labels", "start_date", "target_date"], + filters: [ + "priority", + "state", + "cycle", + "module", + "assignees", + "mentions", + "created_by", + "labels", + "start_date", + "target_date", + ], display_properties: true, display_filters: { group_by: ["state", "priority", "labels", "assignees", "created_by"], @@ -377,7 +409,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { }, }, calendar: { - filters: ["priority", "state", "assignees", "mentions", "created_by", "labels", "start_date"], + filters: ["priority", "state", "cycle", "module", "assignees", "mentions", "created_by", "labels", "start_date"], display_properties: true, display_filters: { type: [null, "active", "backlog"], @@ -388,7 +420,18 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { }, }, spreadsheet: { - filters: ["priority", "state", "assignees", "mentions", "created_by", "labels", "start_date", "target_date"], + filters: [ + "priority", + "state", + "cycle", + "module", + "assignees", + "mentions", + "created_by", + "labels", + "start_date", + "target_date", + ], display_properties: true, display_filters: { order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"], @@ -400,7 +443,18 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { }, }, gantt_chart: { - filters: ["priority", "state", "assignees", "mentions", "created_by", "labels", "start_date", "target_date"], + filters: [ + "priority", + "state", + "cycle", + "module", + "assignees", + "mentions", + "created_by", + "labels", + "start_date", + "target_date", + ], display_properties: false, display_filters: { order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"], diff --git a/web/store/issue/cycle/filter.store.ts b/web/store/issue/cycle/filter.store.ts index b938a36d4..5d8c2a6b8 100644 --- a/web/store/issue/cycle/filter.store.ts +++ b/web/store/issue/cycle/filter.store.ts @@ -84,6 +84,8 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "issues"); if (!filteredParams) return undefined; + if (filteredParams.includes("cycle")) filteredParams.splice(filteredParams.indexOf("cycle"), 1); + const filteredRouteParams: Partial> = this.computedFilteredParams( userFilters?.filters as IIssueFilterOptions, userFilters?.displayFilters as IIssueDisplayFilterOptions, diff --git a/web/store/issue/helpers/issue-filter-helper.store.ts b/web/store/issue/helpers/issue-filter-helper.store.ts index 8ff45ed09..baac4a2ad 100644 --- a/web/store/issue/helpers/issue-filter-helper.store.ts +++ b/web/store/issue/helpers/issue-filter-helper.store.ts @@ -74,6 +74,8 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore { mentions: filters?.mentions || undefined, created_by: filters?.created_by || undefined, labels: filters?.labels || undefined, + cycle: filters?.cycle || undefined, + module: filters?.module || undefined, start_date: filters?.start_date || undefined, target_date: filters?.target_date || undefined, project: filters.project || undefined, @@ -107,6 +109,8 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore { mentions: filters?.mentions || null, created_by: filters?.created_by || null, labels: filters?.labels || null, + cycle: filters?.cycle || null, + module: filters?.module || null, start_date: filters?.start_date || null, target_date: filters?.target_date || null, project: filters?.project || null, diff --git a/web/store/issue/module/filter.store.ts b/web/store/issue/module/filter.store.ts index f10a885a3..c353059ef 100644 --- a/web/store/issue/module/filter.store.ts +++ b/web/store/issue/module/filter.store.ts @@ -84,6 +84,8 @@ export class ModuleIssuesFilter extends IssueFilterHelperStore implements IModul const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "issues"); if (!filteredParams) return undefined; + if (filteredParams.includes("module")) filteredParams.splice(filteredParams.indexOf("module"), 1); + const filteredRouteParams: Partial> = this.computedFilteredParams( userFilters?.filters as IIssueFilterOptions, userFilters?.displayFilters as IIssueDisplayFilterOptions, From 34301e43999032266d054447b38deb407494f1cd Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 28 Feb 2024 19:55:22 +0530 Subject: [PATCH 005/308] fix: app sidebar toggle (#3829) --- .../sidebar/sidebar-menu-hamburger-toggle.tsx | 8 ++-- web/components/project/sidebar-list-item.tsx | 2 +- web/components/workspace/help-section.tsx | 33 +++++++------ web/components/workspace/sidebar-dropdown.tsx | 7 ++- web/components/workspace/sidebar-menu.tsx | 11 +++-- web/layouts/app-layout/sidebar.tsx | 8 ++-- .../settings-layout/profile/sidebar.tsx | 48 +++++++++++-------- web/store/application/theme.store.ts | 18 ------- 8 files changed, 64 insertions(+), 71 deletions(-) diff --git a/web/components/core/sidebar/sidebar-menu-hamburger-toggle.tsx b/web/components/core/sidebar/sidebar-menu-hamburger-toggle.tsx index cb433de05..0212e4980 100644 --- a/web/components/core/sidebar/sidebar-menu-hamburger-toggle.tsx +++ b/web/components/core/sidebar/sidebar-menu-hamburger-toggle.tsx @@ -5,17 +5,17 @@ import { observer } from "mobx-react"; type Props = { onClick?: () => void; -} +}; export const SidebarHamburgerToggle: FC = observer((props) => { - const { onClick } = props + const { onClick } = props; const { theme: themeStore } = useApplication(); return (
{ - if (onClick) onClick() - else themeStore.toggleMobileSidebar() + if (onClick) onClick(); + else themeStore.toggleSidebar(); }} > diff --git a/web/components/project/sidebar-list-item.tsx b/web/components/project/sidebar-list-item.tsx index 3dbf0d5d0..695e0bce4 100644 --- a/web/components/project/sidebar-list-item.tsx +++ b/web/components/project/sidebar-list-item.tsx @@ -144,7 +144,7 @@ export const ProjectSidebarListItem: React.FC = observer((props) => { const handleProjectClick = () => { if (window.innerWidth < 768) { - themeStore.toggleMobileSidebar(); + themeStore.toggleSidebar(); } }; diff --git a/web/components/workspace/help-section.tsx b/web/components/workspace/help-section.tsx index a61ac1823..0bb77f9c7 100644 --- a/web/components/workspace/help-section.tsx +++ b/web/components/workspace/help-section.tsx @@ -10,7 +10,6 @@ import { FileText, HelpCircle, MessagesSquare, MoveLeft, Zap } from "lucide-reac import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui"; // assets import packageJson from "package.json"; -import useSize from "hooks/use-window-size"; const helpOptions = [ { @@ -43,11 +42,10 @@ export interface WorkspaceHelpSectionProps { export const WorkspaceHelpSection: React.FC = observer(() => { // store hooks const { - theme: { sidebarCollapsed, toggleSidebar, toggleMobileSidebar }, + theme: { sidebarCollapsed, toggleSidebar }, commandPalette: { toggleShortcutModal }, } = useApplication(); - const [windowWidth] = useSize(); // states const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false); // refs @@ -60,8 +58,9 @@ export const WorkspaceHelpSection: React.FC = observe return ( <>
{!isCollapsed && (
@@ -72,8 +71,9 @@ export const WorkspaceHelpSection: React.FC = observe @@ -101,9 +102,10 @@ export const WorkspaceHelpSection: React.FC = observe @@ -121,8 +123,9 @@ export const WorkspaceHelpSection: React.FC = observe leaveTo="transform opacity-0 scale-95" >
diff --git a/web/components/workspace/sidebar-dropdown.tsx b/web/components/workspace/sidebar-dropdown.tsx index aeb0a34c2..984bc1caf 100644 --- a/web/components/workspace/sidebar-dropdown.tsx +++ b/web/components/workspace/sidebar-dropdown.tsx @@ -8,7 +8,7 @@ import { mutate } from "swr"; import { Check, ChevronDown, CircleUserRound, LogOut, Mails, PlusSquare, Settings, UserCircle2 } from "lucide-react"; import { usePopper } from "react-popper"; // hooks -import { useApplication, useEventTracker, useUser, useWorkspace } from "hooks/store"; +import { useApplication, useUser, useWorkspace } from "hooks/store"; // hooks import useToast from "hooks/use-toast"; // ui @@ -54,9 +54,8 @@ export const WorkspaceSidebarDropdown = observer(() => { const { workspaceSlug } = router.query; // store hooks const { - theme: { sidebarCollapsed, toggleMobileSidebar }, + theme: { sidebarCollapsed, toggleSidebar }, } = useApplication(); - const { setTrackElement } = useEventTracker(); const { currentUser, updateCurrentUser, isUserInstanceAdmin, signOut } = useUser(); const { currentWorkspace: activeWorkspace, workspaces } = useWorkspace(); // hooks @@ -98,7 +97,7 @@ export const WorkspaceSidebarDropdown = observer(() => { }; const handleItemClick = () => { if (window.innerWidth < 768) { - toggleMobileSidebar(); + toggleSidebar(); } }; const workspacesList = Object.values(workspaces ?? {}); diff --git a/web/components/workspace/sidebar-menu.tsx b/web/components/workspace/sidebar-menu.tsx index 9f3f5e1d6..774a231db 100644 --- a/web/components/workspace/sidebar-menu.tsx +++ b/web/components/workspace/sidebar-menu.tsx @@ -31,7 +31,7 @@ export const WorkspaceSidebarMenu = observer(() => { const handleLinkClick = (itemKey: string) => { if (window.innerWidth < 768) { - themeStore.toggleMobileSidebar(); + themeStore.toggleSidebar(); } captureEvent(SIDEBAR_CLICKED, { destination: itemKey, @@ -52,10 +52,11 @@ export const WorkspaceSidebarMenu = observer(() => { disabled={!themeStore?.sidebarCollapsed} >
{ = observer(() => { const ref = useRef(null); useOutsideClickDetector(ref, () => { - if (themStore.mobileSidebarCollapsed === false) { + if (themStore.sidebarCollapsed === false) { if (window.innerWidth < 768) { - themStore.toggleMobileSidebar(); + themStore.toggleSidebar(); } } }); @@ -31,8 +31,8 @@ export const AppSidebar: FC = observer(() => {
{ const { setToastAlert } = useToast(); // store hooks const { - theme: { sidebarCollapsed, toggleSidebar, toggleMobileSidebar }, + theme: { sidebarCollapsed, toggleSidebar }, } = useApplication(); const { currentUser, currentUserSettings, signOut } = useUser(); const { workspaces } = useWorkspace(); @@ -78,7 +78,7 @@ export const ProfileLayoutSidebar = observer(() => { const handleItemClick = () => { if (window.innerWidth < 768) { - toggleMobileSidebar(); + toggleSidebar(); } }; @@ -113,8 +113,9 @@ export const ProfileLayoutSidebar = observer(() => {
@@ -136,10 +137,11 @@ export const ProfileLayoutSidebar = observer(() => {
{} {!sidebarCollapsed && link.label} @@ -160,17 +162,20 @@ export const ProfileLayoutSidebar = observer(() => { {workspace?.logo && workspace.logo !== "" ? ( {
{} {!sidebarCollapsed && link.label} @@ -208,8 +214,9 @@ export const ProfileLayoutSidebar = observer(() => {
diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index 0a36c133b..7cdc23133 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -244,6 +244,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { handleDisplayFiltersUpdate={handleDisplayFilters} displayProperties={issueFilters?.displayProperties ?? {}} handleDisplayPropertiesUpdate={handleDisplayProperties} + ignoreGroupedFilters={["cycle"]} /> diff --git a/web/components/headers/module-issues.tsx b/web/components/headers/module-issues.tsx index f722b506f..b84504ee2 100644 --- a/web/components/headers/module-issues.tsx +++ b/web/components/headers/module-issues.tsx @@ -248,6 +248,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => { handleDisplayFiltersUpdate={handleDisplayFilters} displayProperties={issueFilters?.displayProperties ?? {}} handleDisplayPropertiesUpdate={handleDisplayProperties} + ignoreGroupedFilters={["module"]} />
diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx index 3c94b4f3f..131bea46b 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx @@ -11,7 +11,7 @@ import { FilterSubGroupBy, } from "components/issues"; // types -import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssueGroupByOptions } from "@plane/types"; import { ILayoutDisplayFiltersOptions } from "constants/issue"; type Props = { @@ -20,6 +20,7 @@ type Props = { handleDisplayFiltersUpdate: (updatedDisplayFilter: Partial) => void; handleDisplayPropertiesUpdate: (updatedDisplayProperties: Partial) => void; layoutDisplayFiltersOptions: ILayoutDisplayFiltersOptions | undefined; + ignoreGroupedFilters?: Partial[]; }; export const DisplayFiltersSelection: React.FC = observer((props) => { @@ -29,6 +30,7 @@ export const DisplayFiltersSelection: React.FC = observer((props) => { handleDisplayFiltersUpdate, handleDisplayPropertiesUpdate, layoutDisplayFiltersOptions, + ignoreGroupedFilters = [], } = props; const isDisplayFilterEnabled = (displayFilter: keyof IIssueDisplayFilterOptions) => @@ -54,6 +56,7 @@ export const DisplayFiltersSelection: React.FC = observer((props) => { group_by: val, }) } + ignoreGroupedFilters={ignoreGroupedFilters} />
)} @@ -71,6 +74,7 @@ export const DisplayFiltersSelection: React.FC = observer((props) => { }) } subGroupByOptions={layoutDisplayFiltersOptions?.display_filters.sub_group_by ?? []} + ignoreGroupedFilters={ignoreGroupedFilters} />
)} diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx index 659d86d08..a4478e834 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx @@ -1,6 +1,5 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; - // components import { FilterHeader, FilterOption } from "components/issues"; // types @@ -12,10 +11,11 @@ type Props = { displayFilters: IIssueDisplayFilterOptions; groupByOptions: TIssueGroupByOptions[]; handleUpdate: (val: TIssueGroupByOptions) => void; + ignoreGroupedFilters: Partial[]; }; export const FilterGroupBy: React.FC = observer((props) => { - const { displayFilters, groupByOptions, handleUpdate } = props; + const { displayFilters, groupByOptions, handleUpdate, ignoreGroupedFilters } = props; const [previewEnabled, setPreviewEnabled] = useState(true); @@ -34,6 +34,7 @@ export const FilterGroupBy: React.FC = observer((props) => { {ISSUE_GROUP_BY_OPTIONS.filter((option) => groupByOptions.includes(option.key)).map((groupBy) => { if (displayFilters.layout === "kanban" && selectedSubGroupBy !== null && groupBy.key === selectedSubGroupBy) return null; + if (ignoreGroupedFilters.includes(groupBy?.key)) return null; return ( void; subGroupByOptions: TIssueGroupByOptions[]; + ignoreGroupedFilters: Partial[]; }; export const FilterSubGroupBy: React.FC = observer((props) => { - const { displayFilters, handleUpdate, subGroupByOptions } = props; + const { displayFilters, handleUpdate, subGroupByOptions, ignoreGroupedFilters } = props; const [previewEnabled, setPreviewEnabled] = useState(true); @@ -33,6 +33,7 @@ export const FilterSubGroupBy: React.FC = observer((props) => {
{ISSUE_GROUP_BY_OPTIONS.filter((option) => subGroupByOptions.includes(option.key)).map((subGroupBy) => { if (selectedGroupBy !== null && subGroupBy.key === selectedGroupBy) return null; + if (ignoreGroupedFilters.includes(subGroupBy?.key)) return null; return ( = observer((props) => { const member = useMember(); const project = useProject(); const label = useLabel(); + const cycle = useCycle(); + const _module = useModule(); const projectState = useProjectState(); const { peekIssue } = useIssueDetail(); - const list = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member); + const list = getGroupByColumns(group_by as GroupByColumnTypes, project, cycle, _module, label, projectState, member); if (!list) return null; - const groupWithIssues = list.filter((_list) => (issueIds as TGroupedIssues)[_list.id]?.length > 0); + const groupWithIssues = list.filter((_list) => (issueIds as TGroupedIssues)?.[_list.id]?.length > 0); const groupList = showEmptyGroup ? list : groupWithIssues; diff --git a/web/components/issues/issue-layouts/kanban/kanban-group.tsx b/web/components/issues/issue-layouts/kanban/kanban-group.tsx index 7cbda05e1..a2f9ec4fb 100644 --- a/web/components/issues/issue-layouts/kanban/kanban-group.tsx +++ b/web/components/issues/issue-layouts/kanban/kanban-group.tsx @@ -80,6 +80,10 @@ export const KanbanGroup = (props: IKanbanGroup) => { preloadedData = { ...preloadedData, state_id: groupValue }; } else if (groupByKey === "priority") { preloadedData = { ...preloadedData, priority: groupValue }; + } else if (groupByKey === "cycle") { + preloadedData = { ...preloadedData, cycle_id: groupValue }; + } else if (groupByKey === "module") { + preloadedData = { ...preloadedData, module_ids: [groupValue] }; } else if (groupByKey === "labels" && groupValue != "None") { preloadedData = { ...preloadedData, label_ids: [groupValue] }; } else if (groupByKey === "assignees" && groupValue != "None") { @@ -96,6 +100,10 @@ export const KanbanGroup = (props: IKanbanGroup) => { preloadedData = { ...preloadedData, state_id: subGroupValue }; } else if (subGroupByKey === "priority") { preloadedData = { ...preloadedData, priority: subGroupValue }; + } else if (groupByKey === "cycle") { + preloadedData = { ...preloadedData, cycle_id: subGroupValue }; + } else if (groupByKey === "module") { + preloadedData = { ...preloadedData, module_ids: [subGroupValue] }; } else if (subGroupByKey === "labels" && subGroupValue != "None") { preloadedData = { ...preloadedData, label_ids: [subGroupValue] }; } else if (subGroupByKey === "assignees" && subGroupValue != "None") { diff --git a/web/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/components/issues/issue-layouts/kanban/swimlanes.tsx index 5fdb58ef0..7b57752f2 100644 --- a/web/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/web/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -18,7 +18,7 @@ import { } from "@plane/types"; // constants import { EIssueActions } from "../types"; -import { useLabel, useMember, useProject, useProjectState } from "hooks/store"; +import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "hooks/store"; import { getGroupByColumns } from "../utils"; import { TCreateModalStoreTypes } from "constants/issue"; @@ -217,10 +217,28 @@ export const KanBanSwimLanes: React.FC = observer((props) => { const member = useMember(); const project = useProject(); const label = useLabel(); + const cycle = useCycle(); + const _module = useModule(); const projectState = useProjectState(); - const groupByList = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member); - const subGroupByList = getGroupByColumns(sub_group_by as GroupByColumnTypes, project, label, projectState, member); + const groupByList = getGroupByColumns( + group_by as GroupByColumnTypes, + project, + cycle, + _module, + label, + projectState, + member + ); + const subGroupByList = getGroupByColumns( + sub_group_by as GroupByColumnTypes, + project, + cycle, + _module, + label, + projectState, + member + ); if (!groupByList || !subGroupByList) return null; diff --git a/web/components/issues/issue-layouts/list/default.tsx b/web/components/issues/issue-layouts/list/default.tsx index e148d5131..c6f82c2be 100644 --- a/web/components/issues/issue-layouts/list/default.tsx +++ b/web/components/issues/issue-layouts/list/default.tsx @@ -3,7 +3,7 @@ import { useRef } from "react"; import { IssueBlocksList, ListQuickAddIssueForm } from "components/issues"; import { HeaderGroupByCard } from "./headers/group-by-card"; // hooks -import { useLabel, useMember, useProject, useProjectState } from "hooks/store"; +import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "hooks/store"; // types import { GroupByColumnTypes, @@ -65,10 +65,21 @@ const GroupByList: React.FC = (props) => { const project = useProject(); const label = useLabel(); const projectState = useProjectState(); + const cycle = useCycle(); + const _module = useModule(); const containerRef = useRef(null); - const groups = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member, true); + const groups = getGroupByColumns( + group_by as GroupByColumnTypes, + project, + cycle, + _module, + label, + projectState, + member, + true + ); if (!groups) return null; diff --git a/web/components/issues/issue-layouts/utils.tsx b/web/components/issues/issue-layouts/utils.tsx index 0c3367dc1..ce49d774d 100644 --- a/web/components/issues/issue-layouts/utils.tsx +++ b/web/components/issues/issue-layouts/utils.tsx @@ -1,16 +1,25 @@ -import { Avatar, PriorityIcon, StateGroupIcon } from "@plane/ui"; -import { EIssueListRow, ISSUE_PRIORITIES } from "constants/issue"; -import { renderEmoji } from "helpers/emoji.helper"; +import { Avatar, CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/ui"; +// stores import { IMemberRootStore } from "store/member"; import { IProjectStore } from "store/project/project.store"; import { IStateStore } from "store/state.store"; -import { GroupByColumnTypes, IGroupByColumn, IIssueListRow, TGroupedIssues, TUnGroupedIssues } from "@plane/types"; -import { STATE_GROUPS } from "constants/state"; import { ILabelStore } from "store/label.store"; +import { ICycleStore } from "store/cycle.store"; +import { IModuleStore } from "store/module.store"; +// helpers +import { renderEmoji } from "helpers/emoji.helper"; +// constants +import { STATE_GROUPS } from "constants/state"; +import { ISSUE_PRIORITIES } from "constants/issue"; +// types +import { GroupByColumnTypes, IGroupByColumn, TCycleGroups } from "@plane/types"; +import { ContrastIcon } from "lucide-react"; export const getGroupByColumns = ( groupBy: GroupByColumnTypes | null, project: IProjectStore, + cycle: ICycleStore, + module: IModuleStore, label: ILabelStore, projectState: IStateStore, member: IMemberRootStore, @@ -19,6 +28,10 @@ export const getGroupByColumns = ( switch (groupBy) { case "project": return getProjectColumns(project); + case "cycle": + return getCycleColumns(project, cycle); + case "module": + return getModuleColumns(project, module); case "state": return getStateColumns(projectState); case "state_detail.group": @@ -55,6 +68,68 @@ const getProjectColumns = (project: IProjectStore): IGroupByColumn[] | undefined }) as any; }; +const getCycleColumns = (projectStore: IProjectStore, cycleStore: ICycleStore): IGroupByColumn[] | undefined => { + const { currentProjectDetails } = projectStore; + const { getProjectCycleIds, getCycleById } = cycleStore; + + if (!currentProjectDetails || !currentProjectDetails?.id) return; + + const cycleIds = currentProjectDetails?.id ? getProjectCycleIds(currentProjectDetails?.id) : undefined; + if (!cycleIds) return; + + const cycles = []; + + cycleIds.map((cycleId) => { + const cycle = getCycleById(cycleId); + if (cycle) { + const cycleStatus = cycle.status ? (cycle.status.toLocaleLowerCase() as TCycleGroups) : "draft"; + cycles.push({ + id: cycle.id, + name: cycle.name, + icon: , + payload: { cycle_id: cycle.id }, + }); + } + }); + cycles.push({ + id: "None", + name: "None", + icon: , + }); + + return cycles as any; +}; + +const getModuleColumns = (projectStore: IProjectStore, moduleStore: IModuleStore): IGroupByColumn[] | undefined => { + const { currentProjectDetails } = projectStore; + const { getProjectModuleIds, getModuleById } = moduleStore; + + if (!currentProjectDetails || !currentProjectDetails?.id) return; + + const moduleIds = currentProjectDetails?.id ? getProjectModuleIds(currentProjectDetails?.id) : undefined; + if (!moduleIds) return; + + const modules = []; + + moduleIds.map((moduleId) => { + const _module = getModuleById(moduleId); + if (_module) + modules.push({ + id: _module.id, + name: _module.name, + icon: , + payload: { module_ids: [_module.id] }, + }); + }) as any; + modules.push({ + id: "None", + name: "None", + icon: , + }); + + return modules as any; +}; + const getStateColumns = (projectState: IStateStore): IGroupByColumn[] | undefined => { const { projectStates } = projectState; if (!projectStates) return; diff --git a/web/components/issues/issues-mobile-header.tsx b/web/components/issues/issues-mobile-header.tsx index 2338e1848..56b88f70c 100644 --- a/web/components/issues/issues-mobile-header.tsx +++ b/web/components/issues/issues-mobile-header.tsx @@ -14,153 +14,153 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption import { ProjectAnalyticsModal } from "components/analytics"; export const IssuesMobileHeader = () => { - const layouts = [ - { key: "list", title: "List", icon: List }, - { key: "kanban", title: "Kanban", icon: Kanban }, - { key: "calendar", title: "Calendar", icon: Calendar }, - ]; - const [analyticsModal, setAnalyticsModal] = useState(false); - const { workspaceSlug, projectId } = router.query as { - workspaceSlug: string; - projectId: string; - }; - const { currentProjectDetails } = useProject(); - const { projectStates } = useProjectState(); - const { projectLabels } = useLabel(); + const layouts = [ + { key: "list", title: "List", icon: List }, + { key: "kanban", title: "Kanban", icon: Kanban }, + { key: "calendar", title: "Calendar", icon: Calendar }, + ]; + const [analyticsModal, setAnalyticsModal] = useState(false); + const { workspaceSlug, projectId } = router.query as { + workspaceSlug: string; + projectId: string; + }; + const { currentProjectDetails } = useProject(); + const { projectStates } = useProjectState(); + const { projectLabels } = useLabel(); - // store hooks - const { - issuesFilter: { issueFilters, updateFilters }, - } = useIssues(EIssuesStoreType.PROJECT); - const { - project: { projectMemberIds }, - } = useMember(); - const activeLayout = issueFilters?.displayFilters?.layout; + // store hooks + const { + issuesFilter: { issueFilters, updateFilters }, + } = useIssues(EIssuesStoreType.PROJECT); + const { + project: { projectMemberIds }, + } = useMember(); + const activeLayout = issueFilters?.displayFilters?.layout; - const handleLayoutChange = useCallback( - (layout: TIssueLayouts) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }); - }, - [workspaceSlug, projectId, updateFilters] - ); + const handleLayoutChange = useCallback( + (layout: TIssueLayouts) => { + if (!workspaceSlug || !projectId) return; + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }); + }, + [workspaceSlug, projectId, updateFilters] + ); - const handleFiltersUpdate = useCallback( - (key: keyof IIssueFilterOptions, value: string | string[]) => { - if (!workspaceSlug || !projectId) return; - const newValues = issueFilters?.filters?.[key] ?? []; + const handleFiltersUpdate = useCallback( + (key: keyof IIssueFilterOptions, value: string | string[]) => { + if (!workspaceSlug || !projectId) return; + const newValues = issueFilters?.filters?.[key] ?? []; - if (Array.isArray(value)) { - value.forEach((val) => { - if (!newValues.includes(val)) newValues.push(val); - }); - } else { - if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); - else newValues.push(value); + if (Array.isArray(value)) { + value.forEach((val) => { + if (!newValues.includes(val)) newValues.push(val); + }); + } else { + if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); + else newValues.push(value); + } + + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }); + }, + [workspaceSlug, projectId, issueFilters, updateFilters] + ); + + const handleDisplayFilters = useCallback( + (updatedDisplayFilter: Partial) => { + if (!workspaceSlug || !projectId) return; + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter); + }, + [workspaceSlug, projectId, updateFilters] + ); + + const handleDisplayProperties = useCallback( + (property: Partial) => { + if (!workspaceSlug || !projectId) return; + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property); + }, + [workspaceSlug, projectId, updateFilters] + ); + + return ( + <> + setAnalyticsModal(false)} + projectDetails={currentProjectDetails ?? undefined} + /> +
+ Layout} + customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm" + closeOnSelect + > + {layouts.map((layout, index) => ( + { + handleLayoutChange(ISSUE_LAYOUTS[index].key); + }} + className="flex items-center gap-2" + > + +
{layout.title}
+
+ ))} +
+
+ + Filters + + } - - updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }); - }, - [workspaceSlug, projectId, issueFilters, updateFilters] - ); - - const handleDisplayFilters = useCallback( - (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter); - }, - [workspaceSlug, projectId, updateFilters] - ); - - const handleDisplayProperties = useCallback( - (property: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property); - }, - [workspaceSlug, projectId, updateFilters] - ); - - return ( - <> - setAnalyticsModal(false)} - projectDetails={currentProjectDetails ?? undefined} + > + -
- Layout} - customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm" - closeOnSelect - > - {layouts.map((layout, index) => ( - { - handleLayoutChange(ISSUE_LAYOUTS[index].key); - }} - className="flex items-center gap-2" - > - -
{layout.title}
-
- ))} -
-
- - Filters - - - } - > - - -
-
- - Display - - - } - > - - -
+ +
+
+ + Display + + + } + > + + +
- -
- - ); + +
+ + ); }; diff --git a/web/components/modules/module-mobile-header.tsx b/web/components/modules/module-mobile-header.tsx index e9ed56a8d..e3f504479 100644 --- a/web/components/modules/module-mobile-header.tsx +++ b/web/components/modules/module-mobile-header.tsx @@ -9,154 +9,155 @@ import router from "next/router"; import { useCallback, useState } from "react"; export const ModuleMobileHeader = () => { - const [analyticsModal, setAnalyticsModal] = useState(false); - const { getModuleById } = useModule(); - const layouts = [ - { key: "list", title: "List", icon: List }, - { key: "kanban", title: "Kanban", icon: Kanban }, - { key: "calendar", title: "Calendar", icon: Calendar }, - ]; - const { workspaceSlug, projectId, moduleId } = router.query as { - workspaceSlug: string; - projectId: string; - moduleId: string; - }; - const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined; + const [analyticsModal, setAnalyticsModal] = useState(false); + const { getModuleById } = useModule(); + const layouts = [ + { key: "list", title: "List", icon: List }, + { key: "kanban", title: "Kanban", icon: Kanban }, + { key: "calendar", title: "Calendar", icon: Calendar }, + ]; + const { workspaceSlug, projectId, moduleId } = router.query as { + workspaceSlug: string; + projectId: string; + moduleId: string; + }; + const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined; - const { - issuesFilter: { issueFilters, updateFilters }, - } = useIssues(EIssuesStoreType.MODULE); - const activeLayout = issueFilters?.displayFilters?.layout; - const { projectStates } = useProjectState(); - const { projectLabels } = useLabel(); - const { - project: { projectMemberIds }, - } = useMember(); + const { + issuesFilter: { issueFilters, updateFilters }, + } = useIssues(EIssuesStoreType.MODULE); + const activeLayout = issueFilters?.displayFilters?.layout; + const { projectStates } = useProjectState(); + const { projectLabels } = useLabel(); + const { + project: { projectMemberIds }, + } = useMember(); - const handleLayoutChange = useCallback( - (layout: TIssueLayouts) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, moduleId); - }, - [workspaceSlug, projectId, moduleId, updateFilters] - ); + const handleLayoutChange = useCallback( + (layout: TIssueLayouts) => { + if (!workspaceSlug || !projectId) return; + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, moduleId); + }, + [workspaceSlug, projectId, moduleId, updateFilters] + ); - const handleFiltersUpdate = useCallback( - (key: keyof IIssueFilterOptions, value: string | string[]) => { - if (!workspaceSlug || !projectId) return; - const newValues = issueFilters?.filters?.[key] ?? []; + const handleFiltersUpdate = useCallback( + (key: keyof IIssueFilterOptions, value: string | string[]) => { + if (!workspaceSlug || !projectId) return; + const newValues = issueFilters?.filters?.[key] ?? []; - if (Array.isArray(value)) { - value.forEach((val) => { - if (!newValues.includes(val)) newValues.push(val); - }); - } else { - if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); - else newValues.push(value); + if (Array.isArray(value)) { + value.forEach((val) => { + if (!newValues.includes(val)) newValues.push(val); + }); + } else { + if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); + else newValues.push(value); + } + + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, moduleId); + }, + [workspaceSlug, projectId, moduleId, issueFilters, updateFilters] + ); + + const handleDisplayFilters = useCallback( + (updatedDisplayFilter: Partial) => { + if (!workspaceSlug || !projectId) return; + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, moduleId); + }, + [workspaceSlug, projectId, moduleId, updateFilters] + ); + + const handleDisplayProperties = useCallback( + (property: Partial) => { + if (!workspaceSlug || !projectId) return; + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, moduleId); + }, + [workspaceSlug, projectId, moduleId, updateFilters] + ); + + return ( +
+ setAnalyticsModal(false)} + moduleDetails={moduleDetails ?? undefined} + /> +
+ Layout} + customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm" + closeOnSelect + > + {layouts.map((layout, index) => ( + { + handleLayoutChange(ISSUE_LAYOUTS[index].key); + }} + className="flex items-center gap-2" + > + +
{layout.title}
+
+ ))} +
+
+ + Filters + + } - - updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, moduleId); - }, - [workspaceSlug, projectId, moduleId, issueFilters, updateFilters] - ); - - const handleDisplayFilters = useCallback( - (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, moduleId); - }, - [workspaceSlug, projectId, moduleId, updateFilters] - ); - - const handleDisplayProperties = useCallback( - (property: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, moduleId); - }, - [workspaceSlug, projectId, moduleId, updateFilters] - ); - - return ( -
- setAnalyticsModal(false)} - moduleDetails={moduleDetails ?? undefined} + > + -
- Layout} - customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm" - closeOnSelect - > - {layouts.map((layout, index) => ( - { - handleLayoutChange(ISSUE_LAYOUTS[index].key); - }} - className="flex items-center gap-2" - > - -
{layout.title}
-
- ))} -
-
- - Filters - - - } - > - - -
-
- - Display - - - } - > - - -
- - -
+
- ); +
+ + Display + + + } + > + + +
+ + +
+
+ ); }; diff --git a/web/constants/issue.ts b/web/constants/issue.ts index f1bcc3a06..7e0fd7a06 100644 --- a/web/constants/issue.ts +++ b/web/constants/issue.ts @@ -49,22 +49,6 @@ export const ISSUE_PRIORITIES: { { key: "none", title: "None" }, ]; -export const ISSUE_START_DATE_OPTIONS = [ - { key: "last_week", title: "Last Week" }, - { key: "2_weeks_from_now", title: "2 weeks from now" }, - { key: "1_month_from_now", title: "1 month from now" }, - { key: "2_months_from_now", title: "2 months from now" }, - { key: "custom", title: "Custom" }, -]; - -export const ISSUE_DUE_DATE_OPTIONS = [ - { key: "last_week", title: "Last Week" }, - { key: "2_weeks_from_now", title: "2 weeks from now" }, - { key: "1_month_from_now", title: "1 month from now" }, - { key: "2_months_from_now", title: "2 months from now" }, - { key: "custom", title: "Custom" }, -]; - export const ISSUE_GROUP_BY_OPTIONS: { key: TIssueGroupByOptions; title: string; @@ -73,6 +57,8 @@ export const ISSUE_GROUP_BY_OPTIONS: { { key: "state_detail.group", title: "State Groups" }, { key: "priority", title: "Priority" }, { key: "project", title: "Project" }, // required this on my issues + { key: "cycle", title: "Cycle" }, // required this on my issues + { key: "module", title: "Module" }, // required this on my issues { key: "labels", title: "Labels" }, { key: "assignees", title: "Assignees" }, { key: "created_by", title: "Created By" }, @@ -140,81 +126,6 @@ export const ISSUE_LAYOUTS: { { key: "gantt_chart", title: "Gantt Chart Layout", icon: GanttChartSquare }, ]; -export const ISSUE_LIST_FILTERS = [ - { key: "mentions", title: "Mentions" }, - { key: "priority", title: "Priority" }, - { key: "state", title: "State" }, - { key: "assignees", title: "Assignees" }, - { key: "created_by", title: "Created By" }, - { key: "labels", title: "Labels" }, - { key: "start_date", title: "Start Date" }, - { key: "due_date", title: "Due Date" }, -]; - -export const ISSUE_KANBAN_FILTERS = [ - { key: "priority", title: "Priority" }, - { key: "state", title: "State" }, - { key: "assignees", title: "Assignees" }, - { key: "created_by", title: "Created By" }, - { key: "labels", title: "Labels" }, - { key: "start_date", title: "Start Date" }, - { key: "due_date", title: "Due Date" }, -]; - -export const ISSUE_CALENDER_FILTERS = [ - { key: "priority", title: "Priority" }, - { key: "state", title: "State" }, - { key: "assignees", title: "Assignees" }, - { key: "created_by", title: "Created By" }, - { key: "labels", title: "Labels" }, -]; - -export const ISSUE_SPREADSHEET_FILTERS = [ - { key: "priority", title: "Priority" }, - { key: "state", title: "State" }, - { key: "assignees", title: "Assignees" }, - { key: "created_by", title: "Created By" }, - { key: "labels", title: "Labels" }, - { key: "start_date", title: "Start Date" }, - { key: "due_date", title: "Due Date" }, -]; - -export const ISSUE_GANTT_FILTERS = [ - { key: "priority", title: "Priority" }, - { key: "state", title: "State" }, - { key: "assignees", title: "Assignees" }, - { key: "created_by", title: "Created By" }, - { key: "labels", title: "Labels" }, - { key: "start_date", title: "Start Date" }, - { key: "due_date", title: "Due Date" }, -]; - -export const ISSUE_LIST_DISPLAY_FILTERS = [ - { key: "group_by", title: "Group By" }, - { key: "order_by", title: "Order By" }, - { key: "issue_type", title: "Issue Type" }, - { key: "sub_issue", title: "Sub Issue" }, - { key: "show_empty_groups", title: "Show Empty Groups" }, -]; - -export const ISSUE_KANBAN_DISPLAY_FILTERS = [ - { key: "group_by", title: "Group By" }, - { key: "order_by", title: "Order By" }, - { key: "issue_type", title: "Issue Type" }, - { key: "sub_issue", title: "Sub Issue" }, - { key: "show_empty_groups", title: "Show Empty Groups" }, -]; - -export const ISSUE_CALENDER_DISPLAY_FILTERS = [{ key: "issue_type", title: "Issue Type" }]; - -export const ISSUE_SPREADSHEET_DISPLAY_FILTERS = [{ key: "issue_type", title: "Issue Type" }]; - -export const ISSUE_GANTT_DISPLAY_FILTERS = [ - { key: "order_by", title: "Order By" }, - { key: "issue_type", title: "Issue Type" }, - { key: "sub_issue", title: "Sub Issue" }, -]; - export interface ILayoutDisplayFiltersOptions { filters: (keyof IIssueFilterOptions)[]; display_properties: boolean; @@ -276,7 +187,17 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { ], display_properties: true, display_filters: { - group_by: ["state", "state_detail.group", "priority", "labels", "assignees", "created_by", null], + group_by: [ + "state", + "cycle", + "module", + "state_detail.group", + "priority", + "labels", + "assignees", + "created_by", + null, + ], order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"], type: [null, "active", "backlog"], }, @@ -291,7 +212,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { filters: ["priority", "state_group", "cycle", "module", "labels", "start_date", "target_date"], display_properties: true, display_filters: { - group_by: ["state_detail.group", "priority", "project", "labels", null], + group_by: ["state_detail.group", "cycle", "module", "priority", "project", "labels", null], order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"], type: [null, "active", "backlog"], }, @@ -304,7 +225,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { filters: ["priority", "state_group", "cycle", "module", "labels", "start_date", "target_date"], display_properties: true, display_filters: { - group_by: ["state_detail.group", "priority", "project", "labels"], + group_by: ["state_detail.group", "cycle", "module", "priority", "project", "labels"], order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"], type: [null, "active", "backlog"], }, @@ -374,7 +295,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { ], display_properties: true, display_filters: { - group_by: ["state", "priority", "labels", "assignees", "created_by", null], + group_by: ["state", "priority", "cycle", "module", "labels", "assignees", "created_by", null], order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"], type: [null, "active", "backlog"], }, @@ -398,8 +319,8 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { ], display_properties: true, display_filters: { - group_by: ["state", "priority", "labels", "assignees", "created_by"], - sub_group_by: ["state", "priority", "labels", "assignees", "created_by", null], + group_by: ["state", "priority", "cycle", "module", "labels", "assignees", "created_by"], + sub_group_by: ["state", "priority", "cycle", "module", "labels", "assignees", "created_by", null], order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority", "target_date"], type: [null, "active", "backlog"], }, diff --git a/web/store/issue/helpers/issue-helper.store.ts b/web/store/issue/helpers/issue-helper.store.ts index 9d498e900..a267ac9c8 100644 --- a/web/store/issue/helpers/issue-helper.store.ts +++ b/web/store/issue/helpers/issue-helper.store.ts @@ -36,6 +36,8 @@ export type TIssueHelperStore = { const ISSUE_FILTER_DEFAULT_DATA: Record = { project: "project_id", + cycle: "cycle_id", + module: "module_ids", state: "state_id", "state_detail.group": "state_group" as keyof TIssue, // state_detail.group is only being used for state_group display, priority: "priority", @@ -157,6 +159,10 @@ export class IssueHelperStore implements TIssueHelperStore { return Object.keys(this.rootStore?.workSpaceMemberRolesMap || {}); case "project": return Object.keys(this.rootStore?.projectMap || {}); + case "cycle": + return Object.keys(this.rootStore?.cycleMap || {}); + case "module": + return Object.keys(this.rootStore?.moduleMap || {}); default: return []; } From 62693abb0992a35f2cd57c7caf8b94304c2756f7 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Thu, 29 Feb 2024 15:31:44 +0530 Subject: [PATCH 009/308] chore: project emoji icon improvement (#3837) --- web/components/emoji-icon-picker/index.tsx | 2 +- web/helpers/emoji.helper.tsx | 19 ++++++++----------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/web/components/emoji-icon-picker/index.tsx b/web/components/emoji-icon-picker/index.tsx index 0c72b986a..57d5d8896 100644 --- a/web/components/emoji-icon-picker/index.tsx +++ b/web/components/emoji-icon-picker/index.tsx @@ -53,7 +53,7 @@ const EmojiIconPicker: React.FC = (props) => { setIsOpen((prev) => !prev)} - className="outline-none" + className="outline-none flex items-center justify-center" disabled={disabled} > {label} diff --git a/web/helpers/emoji.helper.tsx b/web/helpers/emoji.helper.tsx index 026211634..5ff95027b 100644 --- a/web/helpers/emoji.helper.tsx +++ b/web/helpers/emoji.helper.tsx @@ -30,7 +30,7 @@ export const renderEmoji = ( if (typeof emoji === "object") return ( - + {emoji.name} ); @@ -41,16 +41,13 @@ export const groupReactions: (reactions: any[], key: string) => { [key: string]: reactions: any, key: string ) => { - const groupedReactions = reactions.reduce( - (acc: any, reaction: any) => { - if (!acc[reaction[key]]) { - acc[reaction[key]] = []; - } - acc[reaction[key]].push(reaction); - return acc; - }, - {} as { [key: string]: any[] } - ); + const groupedReactions = reactions.reduce((acc: any, reaction: any) => { + if (!acc[reaction[key]]) { + acc[reaction[key]] = []; + } + acc[reaction[key]].push(reaction); + return acc; + }, {} as { [key: string]: any[] }); return groupedReactions; }; From 65024fe5ecaae3fa2e6ebb177b0530e6bc842ac5 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Thu, 29 Feb 2024 16:12:34 +0530 Subject: [PATCH 010/308] chore: priority icon none state hover state added (#3840) --- web/components/dropdowns/priority.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/dropdowns/priority.tsx b/web/components/dropdowns/priority.tsx index 5cacefb3f..e0677c843 100644 --- a/web/components/dropdowns/priority.tsx +++ b/web/components/dropdowns/priority.tsx @@ -58,7 +58,7 @@ const BorderButton = (props: ButtonProps) => { high: "bg-orange-500/20 text-orange-950 border-orange-500", medium: "bg-yellow-500/20 text-yellow-950 border-yellow-500", low: "bg-custom-primary-100/20 text-custom-primary-950 border-custom-primary-100", - none: "bg-custom-background-80 border-custom-border-300", + none: "hover:bg-custom-background-80 border-custom-border-300", }; return ( @@ -197,7 +197,7 @@ const TransparentButton = (props: ButtonProps) => { high: "text-orange-950", medium: "text-yellow-950", low: "text-blue-950", - none: "", + none: "hover:text-custom-text-300", }; return ( From d1087820f6d5e3c75e13ba49d2281c408f3e8c69 Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Thu, 29 Feb 2024 17:18:03 +0530 Subject: [PATCH 011/308] [web-599] ui: kanban layout UI consistency enhancement for grouped display filters (#3841) * ui: UI inconsistancy in kanban layout when we grouped and sub grouped display filters. * ui: width update in the kanban block --- .../issue-layouts/kanban/base-kanban-root.tsx | 44 ++++++++++--------- .../issues/issue-layouts/kanban/block.tsx | 2 +- .../issues/issue-layouts/kanban/default.tsx | 4 +- .../issue-layouts/kanban/kanban-group.tsx | 4 +- .../issues/issue-layouts/kanban/swimlanes.tsx | 4 +- 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx index 0d7a984b1..7bdaf282d 100644 --- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -291,27 +291,29 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas
- +
+ +
diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index 8446e7328..be27f7706 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -141,7 +141,7 @@ export const KanbanIssueBlock: React.FC = memo((props) => { >
= observer((props) => { const isGroupByCreatedBy = group_by === "created_by"; return ( -
+
{groupList && groupList.length > 0 && groupList.map((_list: IGroupByColumn) => { const groupByVisibilityToggle = visibilityGroupBy(_list); return ( -
+
{sub_group_by === null && (
{ {(provided: any, snapshot: any) => (
diff --git a/web/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/components/issues/issue-layouts/kanban/swimlanes.tsx index 7b57752f2..44715cf62 100644 --- a/web/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/web/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -38,11 +38,11 @@ const SubGroupSwimlaneHeader: React.FC = ({ kanbanFilters, handleKanbanFilters, }) => ( -
+
{list && list.length > 0 && list.map((_list: IGroupByColumn) => ( -
+
Date: Thu, 29 Feb 2024 17:19:13 +0530 Subject: [PATCH 012/308] fix: project sidebar favorite list logic updated (#3842) --- web/store/project/project.store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/store/project/project.store.ts b/web/store/project/project.store.ts index c9aa826fe..176c3a364 100644 --- a/web/store/project/project.store.ts +++ b/web/store/project/project.store.ts @@ -148,7 +148,7 @@ export class ProjectStore implements IProjectStore { projects = sortBy(projects, "created_at"); const projectIds = projects - .filter((project) => project.workspace === currentWorkspace.id && project.is_favorite) + .filter((project) => project.workspace === currentWorkspace.id && project.is_member && project.is_favorite) .map((project) => project.id); return projectIds; } From 5d7c0a2a64d1d1681918da46f4e5b5d31876138d Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Thu, 29 Feb 2024 17:19:51 +0530 Subject: [PATCH 013/308] [WEB-600] fix: fixed sub-group-by issue count display in kanban layout header (#3843) * fix: fixed subgroupby issue count at the header in kanban layout * chore: code beautification --- .../issues/issue-layouts/kanban/swimlanes.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/web/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/components/issues/issue-layouts/kanban/swimlanes.tsx index 7b57752f2..1102cdcd6 100644 --- a/web/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/web/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -30,6 +30,15 @@ interface ISubGroupSwimlaneHeader { kanbanFilters: TIssueKanbanFilters; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; } + +const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: string) => { + let headerCount = 0; + Object.keys(issueIds).map((groupState) => { + headerCount = headerCount + (issueIds?.[groupState]?.[groupById]?.length || 0); + }); + return headerCount; +}; + const SubGroupSwimlaneHeader: React.FC = ({ issueIds, sub_group_by, @@ -49,7 +58,7 @@ const SubGroupSwimlaneHeader: React.FC = ({ column_id={_list.id} icon={_list.icon} title={_list.name} - count={(issueIds as TGroupedIssues)?.[_list.id]?.length || 0} + count={getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, _list?.id)} kanbanFilters={kanbanFilters} handleKanbanFilters={handleKanbanFilters} issuePayload={_list.payload} From f183e389eab48937e9e389a7c656bbc90cb12ddd Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Thu, 29 Feb 2024 17:20:17 +0530 Subject: [PATCH 014/308] fix: module sidebar description duplicacy (#3844) --- web/components/modules/sidebar.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/web/components/modules/sidebar.tsx b/web/components/modules/sidebar.tsx index 53d7eff4c..c8c55321c 100644 --- a/web/components/modules/sidebar.tsx +++ b/web/components/modules/sidebar.tsx @@ -368,12 +368,6 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => {
- {moduleDetails.description && ( - - {moduleDetails.description} - - )} -
From e4988ee9406fa6ea8d90e5f21a2068ed5186dd84 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Thu, 29 Feb 2024 19:40:46 +0530 Subject: [PATCH 015/308] fix: issue sidebar and peek overview cycle select improvement (#3845) --- web/components/issues/issue-detail/cycle-select.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/issues/issue-detail/cycle-select.tsx b/web/components/issues/issue-detail/cycle-select.tsx index fb8449d6f..84dccefac 100644 --- a/web/components/issues/issue-detail/cycle-select.tsx +++ b/web/components/issues/issue-detail/cycle-select.tsx @@ -50,7 +50,7 @@ export const IssueCycleSelect: React.FC = observer((props) => buttonVariant="transparent-with-text" className="w-full group" buttonContainerClassName="w-full text-left" - buttonClassName={`text-sm ${issue?.cycle_id ? "" : "text-custom-text-400"}`} + buttonClassName={`text-sm justify-between ${issue?.cycle_id ? "" : "text-custom-text-400"}`} placeholder="No cycle" hideIcon dropdownArrow From 5cfebb8dae33191fba9f2fbffeea3e762ca271d5 Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Thu, 29 Feb 2024 19:41:30 +0530 Subject: [PATCH 016/308] fix: issue description on last draft issue (#3846) --- web/components/issues/issue-modal/modal.tsx | 3 ++- web/components/workspace/sidebar-quick-action.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/web/components/issues/issue-modal/modal.tsx b/web/components/issues/issue-modal/modal.tsx index dc73bc77a..1da02f0ac 100644 --- a/web/components/issues/issue-modal/modal.tsx +++ b/web/components/issues/issue-modal/modal.tsx @@ -100,8 +100,9 @@ export const CreateUpdateIssueModal: React.FC = observer((prop const fetchIssueDetail = async (issueId: string | undefined) => { if (!workspaceSlug) return; + if (!projectId || issueId === undefined) { - setDescription("

"); + setDescription(data?.description_html || "

"); return; } const response = await fetchIssue(workspaceSlug, projectId, issueId, isDraft ? "DRAFT" : "DEFAULT"); diff --git a/web/components/workspace/sidebar-quick-action.tsx b/web/components/workspace/sidebar-quick-action.tsx index 38d3e6b6a..dd2dd5c68 100644 --- a/web/components/workspace/sidebar-quick-action.tsx +++ b/web/components/workspace/sidebar-quick-action.tsx @@ -58,6 +58,7 @@ export const WorkspaceSidebarQuickAction = observer(() => { const draftIssues = storedValue ?? {}; if (workspaceSlug && draftIssues[workspaceSlug]) delete draftIssues[workspaceSlug]; setValue(draftIssues); + return Promise.resolve(); }; return ( @@ -66,7 +67,7 @@ export const WorkspaceSidebarQuickAction = observer(() => { isOpen={isDraftIssueModalOpen} onClose={() => setIsDraftIssueModalOpen(false)} data={workspaceDraftIssue ?? {}} - // storeType={storeType} + onSubmit={() => removeWorkspaceDraftIssue()} isDraft={true} /> From e6f33eb26252b69055c12ffb885e1c6bccbb693d Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Thu, 29 Feb 2024 20:29:02 +0530 Subject: [PATCH 017/308] [WEB-609] fix: header text overflow issue in kanban layout (#3848) * fix: kanban header text overflow * chore: updated condition --- .../issues/issue-layouts/kanban/default.tsx | 11 +++++++++-- .../issue-layouts/kanban/headers/group-by-card.tsx | 14 +++++++++++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx index 7aac11c6f..3cbab589f 100644 --- a/web/components/issues/issue-layouts/kanban/default.tsx +++ b/web/components/issues/issue-layouts/kanban/default.tsx @@ -101,8 +101,15 @@ const GroupByKanBan: React.FC = observer((props) => { const groupList = showEmptyGroup ? list : groupWithIssues; - const visibilityGroupBy = (_list: IGroupByColumn) => - sub_group_by ? false : kanbanFilters?.group_by.includes(_list.id) ? true : false; + const visibilityGroupBy = (_list: IGroupByColumn) => { + if (sub_group_by) { + if (kanbanFilters?.sub_group_by.includes(_list.id)) return true; + return false; + } else { + if (kanbanFilters?.group_by.includes(_list.id)) return true; + return false; + } + }; const isGroupByCreatedBy = group_by === "created_by"; diff --git a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx index 440b379b8..f49af2922 100644 --- a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx @@ -106,13 +106,21 @@ export const HeaderGroupByCard: FC = observer((props) => { {icon ? icon : }
-
+
{title}
-
+
{count || 0}
From bc6e48fcd6f64aa364f7a5f7fd2cfeeba6e18259 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Fri, 1 Mar 2024 13:33:23 +0530 Subject: [PATCH 018/308] chore: issue comment PATCH changes (#3850) --- apiserver/plane/api/views/issue.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 0905ae1f7..bf3313779 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -716,9 +716,9 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView): # Validation check if the issue already exists if ( - str(request.data.get("external_id")) + request.data.get("external_id") and (issue_comment.external_id != str(request.data.get("external_id"))) - and Issue.objects.filter( + and IssueComment.objects.filter( project_id=project_id, workspace__slug=slug, external_source=request.data.get( From d39f2526a28169a763f48ecf41b5f31353b4777f Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Fri, 1 Mar 2024 13:49:50 +0530 Subject: [PATCH 019/308] chore: removing unnecessary lines --- web/layouts/app-layout/layout.tsx | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/web/layouts/app-layout/layout.tsx b/web/layouts/app-layout/layout.tsx index 2a788e761..07ec9711d 100644 --- a/web/layouts/app-layout/layout.tsx +++ b/web/layouts/app-layout/layout.tsx @@ -6,11 +6,6 @@ import { CommandPalette } from "components/command-palette"; import { AppSidebar } from "./sidebar"; import { observer } from "mobx-react-lite"; -// FIXME: remove this later -import { useIssues } from "hooks/store/use-issues"; -import { EIssuesStoreType } from "constants/issue"; -import useSWR from "swr"; - export interface IAppLayout { children: ReactNode; header: ReactNode; @@ -20,22 +15,6 @@ export interface IAppLayout { export const AppLayout: FC = observer((props) => { const { children, header, withProjectWrapper = false } = props; - const workspaceSlug = "plane-demo"; - const projectId = "b16907a9-a55f-4f5b-b05e-7065a0869ba6"; - - const { issues, issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED); - - useSWR( - workspaceSlug && projectId ? `PROJECT_ARCHIVED_ISSUES_V3_${workspaceSlug}_${projectId}` : null, - async () => { - if (workspaceSlug && projectId) { - await issuesFilter?.fetchFilters(workspaceSlug, projectId); - // await issues?.fetchIssues(workspaceSlug, projectId, issues?.groupedIssueIds ? "mutation" : "init-loader"); - } - }, - { revalidateIfStale: false, revalidateOnFocus: false } - ); - return ( <> From e4bccea82447974f38de10e1fb3079259126fd87 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 1 Mar 2024 17:20:36 +0530 Subject: [PATCH 020/308] fix: multiple issue comment (#3854) --- .../issue-detail/issue-activity/comments/comment-create.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/issues/issue-detail/issue-activity/comments/comment-create.tsx b/web/components/issues/issue-detail/issue-activity/comments/comment-create.tsx index f666b6c1d..af4732808 100644 --- a/web/components/issues/issue-detail/issue-activity/comments/comment-create.tsx +++ b/web/components/issues/issue-detail/issue-activity/comments/comment-create.tsx @@ -74,7 +74,7 @@ export const IssueCommentCreate: FC = (props) => { return (
{ - if (e.key === "Enter" && !e.shiftKey && !isEmpty) { + if (e.key === "Enter" && !e.shiftKey && !isEmpty && !isSubmitting) { handleSubmit(onSubmit)(e); } }} From 4b706437d71732b8ab10dad50751c6407b817a18 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 1 Mar 2024 17:23:26 +0530 Subject: [PATCH 021/308] [WEB-619] fix: workspace all issue quick action (#3853) * chore: custom menu dropdown menu items classname prop added * fix: issue layout quick action z index fix --- packages/ui/src/dropdowns/custom-menu.tsx | 3 ++- packages/ui/src/dropdowns/helper.tsx | 1 + .../issues/issue-layouts/quick-action-dropdowns/all-issue.tsx | 1 + .../issue-layouts/quick-action-dropdowns/archived-issue.tsx | 1 + .../issue-layouts/quick-action-dropdowns/cycle-issue.tsx | 1 + .../issue-layouts/quick-action-dropdowns/module-issue.tsx | 1 + .../issue-layouts/quick-action-dropdowns/project-issue.tsx | 1 + 7 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/dropdowns/custom-menu.tsx b/packages/ui/src/dropdowns/custom-menu.tsx index cdfccbb4e..d1623dddf 100644 --- a/packages/ui/src/dropdowns/custom-menu.tsx +++ b/packages/ui/src/dropdowns/custom-menu.tsx @@ -27,6 +27,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => { noBorder = false, noChevron = false, optionsClassName = "", + menuItemsClassName = "", verticalEllipsis = false, portalElement, menuButtonOnClick, @@ -70,7 +71,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => { useOutsideClickDetector(dropdownRef, closeDropdown); let menuItems = ( - +
void; + menuItemsClassName?: string; onMenuClose?: () => void; closeOnSelect?: boolean; portalElement?: Element | null; diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx index 1d0472454..20d21dc5f 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx @@ -96,6 +96,7 @@ export const AllIssueQuickActions: React.FC = observer((props storeType={EIssuesStoreType.PROJECT} /> = (props) => onSubmit={handleDelete} /> = observer((pro storeType={EIssuesStoreType.CYCLE} /> = observer((pr storeType={EIssuesStoreType.MODULE} /> = observer((p isDraft={isDraftIssue} /> Date: Fri, 1 Mar 2024 17:29:30 +0530 Subject: [PATCH 022/308] [WEB-616] fix: inbox issue navigation issue title and description event propagation (#3851) * fix: inbox issue navigation issue title and description event propagation * fix: inbox issue navigation issue title and description event propagation --- web/components/inbox/inbox-issue-actions.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/components/inbox/inbox-issue-actions.tsx b/web/components/inbox/inbox-issue-actions.tsx index 8a3bb4261..48d9157c6 100644 --- a/web/components/inbox/inbox-issue-actions.tsx +++ b/web/components/inbox/inbox-issue-actions.tsx @@ -131,6 +131,8 @@ export const InboxIssueActionsHeader: FC = observer((p const handleInboxIssueNavigation = useCallback( (direction: "next" | "prev") => { if (!inboxIssues || !inboxIssueId) return; + const activeElement = document.activeElement as HTMLElement; + if (activeElement && (activeElement.classList.contains("tiptap") || activeElement.id === "title-input")) return; const nextIssueIndex = direction === "next" ? (currentIssueIndex + 1) % inboxIssues.length From 59c9b3bdcea56640c95f7d4df0cbeddd9e378544 Mon Sep 17 00:00:00 2001 From: Henit Chobisa Date: Mon, 4 Mar 2024 15:13:39 +0530 Subject: [PATCH 023/308] chore: added auto merge CI for merging sync branches --- .github/workflows/auto-merge.yml | 97 ++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 .github/workflows/auto-merge.yml diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml new file mode 100644 index 000000000..60ebe5834 --- /dev/null +++ b/.github/workflows/auto-merge.yml @@ -0,0 +1,97 @@ +name: Auto Merge or Create PR on Push + +on: + workflow_dispatch: + push: + branches: + - "sync/**" + +env: + CURRENT_BRANCH: ${{ github.ref_name }} + SOURCE_BRANCH: ${{ secrets.SYNC_TARGET_BRANCH_NAME }} # The sync branch such as "sync/ce" + TARGET_BRANCH: ${{ secrets.TARGET_BRANCH }} # The target branch that you would like to merge changes like develop + GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} # Personal access token required to modify contents and workflows + REVIEWER: ${{ secrets.REVIEWER }} + +jobs: + Check_Branch: + runs-on: ubuntu-latest + outputs: + BRANCH_MATCH: ${{ steps.check-branch.outputs.MATCH }} + steps: + - name: Check if current branch matches the secret + id: check-branch + run: | + if [ "$CURRENT_BRANCH" = "$SOURCE_BRANCH" ]; then + echo "MATCH=true" >> $GITHUB_OUTPUT + else + echo "MATCH=false" >> $GITHUB_OUTPUT + fi + + Auto_Merge: + if: ${{ needs.Check_Branch.outputs.BRANCH_MATCH == 'true' }} + needs: [Check_Branch] + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: write + steps: + - name: Checkout code + uses: actions/checkout@v4.1.1 + with: + fetch-depth: 0 # Fetch all history for all branches and tags + + - name: Setup GH CLI and Git Config + run: | + type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y) + curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg + sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null + sudo apt update + sudo apt install gh -y + + - id: git-author + name: Setup Git CLI from Github Token + run: | + VIEWER_JSON=$(gh api graphql -f query='query { viewer { name login databaseId }}' --jq '.data.viewer') + VIEWER_NAME=$(jq --raw-output '.name | values' <<< "${VIEWER_JSON}") + VIEWER_LOGIN=$(jq --raw-output '.login' <<< "${VIEWER_JSON}") + VIEWER_DATABASE_ID=$(jq --raw-output '.databaseId' <<< "${VIEWER_JSON}") + + USER_NAME="${VIEWER_NAME:-${VIEWER_LOGIN}}" + USER_EMAIL="${VIEWER_DATABASE_ID}+${VIEWER_LOGIN}@users.noreply.github.com" + + git config --global user.name ${USER_NAME} + git config --global user.email ${USER_EMAIL} + + - name: Check for merge conflicts + id: conflicts + run: | + git fetch origin $TARGET_BRANCH + git checkout $TARGET_BRANCH + # Attempt to merge the main branch into the current branch + if $(git merge --no-commit --no-ff $SOURCE_BRANCH); then + echo "No merge conflicts detected." + echo "HAS_CONFLICTS=false" >> $GITHUB_ENV + else + echo "Merge conflicts detected." + echo "HAS_CONFLICTS=true" >> $GITHUB_ENV + git merge --abort + fi + + - name: Merge Change to Target Branch + if: env.HAS_CONFLICTS == 'false' + run: | + git commit -m "Merge branch '$SOURCE_BRANCH' into $TARGET_BRANCH" + git push origin $TARGET_BRANCH + + - name: Create PR to Target Branch + if: env.HAS_CONFLICTS == 'true' + run: | + # Use GitHub CLI to create PR and specify author and committer + PR_URL=$(gh pr create --base $TARGET_BRANCH --head $SOURCE_BRANCH \ + --title "sync: merge conflicts need to be resolved" \ + --body "" \ + --reviewer $REVIEWER ) + echo "Pull Request created: $PR_URL" + From 6eb7014ea42b637dd46dc4374e821013f832420e Mon Sep 17 00:00:00 2001 From: Vihar Kurama Date: Mon, 4 Mar 2024 20:06:53 +0530 Subject: [PATCH 024/308] chore: update github readme - content edits to feature section, new screenshots and added repo activity, contributors. (#3870) * update github readme, add new features and deployment options * add new product screenshots * add contributors section to readme * chore: update formatting --- README.md | 151 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 98 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index b509fd6f6..52ccda474 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

Plane

-

Flexible, extensible open-source project management

+

Open-source project management that unlocks customer value.

@@ -16,6 +16,13 @@ Commit activity per month

+

+ Website • + Releases • + Twitter • + Documentation +

+

-Meet [Plane](https://plane.so). An open-source software development tool to manage issues, sprints, and product roadmaps with peace of mind 🧘‍♀️. +Meet [Plane](https://plane.so). An open-source software development tool to manage issues, sprints, and product roadmaps with peace of mind. 🧘‍♀️ > Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases. -The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/docker-compose). -## ⚡️ Contributors Quick Start -### Prerequisite +## ⚡ Installation -Development system must have docker engine installed and running. +The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account where we offer a hosted solution for users. -### Steps +If you want more control over your data prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/docker-compose). -Setting up local environment is extremely easy and straight forward. Follow the below step and you will be ready to contribute +| Installation Methods | Documentation Link | +|-----------------|----------------------------------------------------------------------------------------------------------| +| Docker | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://docs.plane.so/docker-compose) | +| Kubernetes | [![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white)](https://docs.plane.so/kubernetes) | -1. Clone the code locally using `git clone https://github.com/makeplane/plane.git` -1. Switch to the code folder `cd plane` -1. Create your feature or fix branch you plan to work on using `git checkout -b ` -1. Open terminal and run `./setup.sh` -1. Open the code on VSCode or similar equivalent IDE -1. Review the `.env` files available in various folders. Visit [Environment Setup](./ENV_SETUP.md) to know about various environment variables used in system -1. Run the docker command to initiate various services `docker compose -f docker-compose-local.yml up -d` - -You are ready to make changes to the code. Do not forget to refresh the browser (in case id does not auto-reload) - -Thats it! - -## 🍙 Self Hosting - -For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/docker-compose) documentation page +`Instance admin` can configure instance settings using our [God-mode](https://docs.plane.so/instance-admin) feature. ## 🚀 Features -- **Issue Planning and Tracking**: Quickly create issues and add details using a powerful rich text editor that supports file uploads. Add sub-properties and references to issues for better organization and tracking. -- **Issue Attachments**: Collaborate effectively by attaching files to issues, making it easy for your team to find and share important project-related documents. -- **Layouts**: Customize your project view with your preferred layout - choose from List, Kanban, or Calendar to visualize your project in a way that makes sense to you. -- **Cycles**: Plan sprints with Cycles to keep your team on track and productive. Gain insights into your project's progress with burn-down charts and other useful features. -- **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to easily track and plan your project's progress. +- **Issues**: Quickly create issues and add details using a powerful, rich text editor that supports file uploads. Add sub-properties and references to problems for better organization and tracking. + +- **Cycles** + Keep up your team's momentum with Cycles. Gain insights into your project's progress with burn-down charts and other valuable features. + +- **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to track and plan your project's progress easily. + - **Views**: Create custom filters to display only the issues that matter to you. Save and share your filters in just a few clicks. -- **Pages**: Plane pages function as an AI-powered notepad, allowing you to easily document issues, cycle plans, and module details, and then synchronize them with your issues. -- **Command K**: Enjoy a better user experience with the new Command + K menu. Easily manage and navigate through your projects from one convenient location. -- **GitHub Sync**: Streamline your planning process by syncing your GitHub issues with Plane. Keep all your issues in one place for better tracking and collaboration. + +- **Pages**: Plane pages, equipped with AI and a rich text editor, let you jot down your thoughts on the fly. Format your text, upload images, hyperlink, or sync your existing ideas into an actionable item or issue. + +- **Analytics**: Get insights into all your Plane data in real-time. Visualize issue data to spot trends, remove blockers, and progress your work. + +- **Drive** (*coming soon*): The drive helps you share documents, images, videos, or any other files that make sense to you or your team and align on the problem/solution. + + + +## 🛠️ Contributors Quick Start + +> Development system must have docker engine installed and running. + +Setting up local environment is extremely easy and straight forward. Follow the below step and you will be ready to contribute + +1. Clone the code locally using: + ``` + git clone https://github.com/makeplane/plane.git + ``` +2. Switch to the code folder: + ``` + cd plane + ``` +3. Create your feature or fix branch you plan to work on using: + ``` + git checkout -b + ``` +4. Open terminal and run: + ``` + ./setup.sh + ``` +5. Open the code on VSCode or similar equivalent IDE. +6. Review the `.env` files available in various folders. + Visit [Environment Setup](./ENV_SETUP.md) to know about various environment variables used in system. +7. Run the docker command to initiate services: + ``` + docker compose -f docker-compose-local.yml up -d + ``` + +You are ready to make changes to the code. Do not forget to refresh the browser (in case it does not auto-reload). + +Thats it! + +## ❤️ Community + +The Plane community can be found on [GitHub Discussions](https://github.com/orgs/makeplane/discussions), and our [Discord server](https://discord.com/invite/A92xrEGCge). Our [Code of conduct](https://github.com/makeplane/plane/blob/master/CODE_OF_CONDUCT.md) applies to all Plane community chanels. + +Ask questions, report bugs, join discussions, voice ideas, make feature requests, or share your projects. + +### Repo Activity +![Plane Repo Activity](https://repobeats.axiom.co/api/embed/2523c6ed2f77c082b7908c33e2ab208981d76c39.svg "Repobeats analytics image") ## 📸 Screenshots

Plane Views @@ -91,8 +135,7 @@ For self hosting environment setup, visit the [Self Hosting](https://docs.plane.

Plane Issue Details @@ -100,7 +143,7 @@ For self hosting environment setup, visit the [Self Hosting](https://docs.plane.

Plane Cycles and Modules @@ -109,7 +152,7 @@ For self hosting environment setup, visit the [Self Hosting](https://docs.plane.

Plane Analytics @@ -118,7 +161,7 @@ For self hosting environment setup, visit the [Self Hosting](https://docs.plane.

Plane Pages @@ -128,7 +171,7 @@ For self hosting environment setup, visit the [Self Hosting](https://docs.plane.

Plane Command Menu @@ -136,20 +179,22 @@ For self hosting environment setup, visit the [Self Hosting](https://docs.plane.

-## 📚Documentation - -For full documentation, visit [docs.plane.so](https://docs.plane.so/) - -To see how to Contribute, visit [here](https://github.com/makeplane/plane/blob/master/CONTRIBUTING.md). - -## ❤️ Community - -The Plane community can be found on GitHub Discussions, where you can ask questions, voice ideas, and share your projects. - -To chat with other community members you can join the [Plane Discord](https://discord.com/invite/A92xrEGCge). - -Our [Code of Conduct](https://github.com/makeplane/plane/blob/master/CODE_OF_CONDUCT.md) applies to all Plane community channels. - ## ⛓️ Security -If you believe you have found a security vulnerability in Plane, we encourage you to responsibly disclose this and not open a public issue. We will investigate all legitimate reports. Email engineering@plane.so to disclose any security vulnerabilities. +If you believe you have found a security vulnerability in Plane, we encourage you to responsibly disclose this and not open a public issue. We will investigate all legitimate reports. + +Email squawk@plane.so to disclose any security vulnerabilities. + +## ❤️ Contribute + +There are many ways to contribute to Plane, including: +- Submitting [bugs](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%F0%9F%90%9Bbug&projects=&template=--bug-report.yaml&title=%5Bbug%5D%3A+) and [feature requests](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%E2%9C%A8feature&projects=&template=--feature-request.yaml&title=%5Bfeature%5D%3A+) for various components. +- Reviewing [the documentation](https://docs.plane.so/) and submitting [pull requests](https://github.com/makeplane/plane), from fixing typos to adding new features. +- Speaking or writing about Plane or any other ecosystem integration and [letting us know](https://discord.com/invite/A92xrEGCge)! +- Upvoting [popular feature requests](https://github.com/makeplane/plane/issues) to show your support. + +### We couldn't have done this without you. + +
+ + \ No newline at end of file From d99529b109fea50d4c74e7fbe7c61b4a23b7bef3 Mon Sep 17 00:00:00 2001 From: "M. Palanikannan" <73993394+Palanikannan1437@users.noreply.github.com> Date: Mon, 4 Mar 2024 20:33:16 +0530 Subject: [PATCH 025/308] fix: crash while updating link text on the last node (#3871) --- .../src/ui/components/links/link-edit-view.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/editor/document-editor/src/ui/components/links/link-edit-view.tsx b/packages/editor/document-editor/src/ui/components/links/link-edit-view.tsx index 136d04e01..971915439 100644 --- a/packages/editor/document-editor/src/ui/components/links/link-edit-view.tsx +++ b/packages/editor/document-editor/src/ui/components/links/link-edit-view.tsx @@ -40,9 +40,11 @@ export const LinkEditView = ({ const [positionRef, setPositionRef] = useState({ from: from, to: to }); const [localUrl, setLocalUrl] = useState(viewProps.url); - const linkRemoved = useRef(); + const linkRemoved = useRef(); const getText = (from: number, to: number) => { + if (to >= editor.state.doc.content.size) return ""; + const text = editor.state.doc.textBetween(from, to, "\n"); return text; }; @@ -72,10 +74,12 @@ export const LinkEditView = ({ const url = isValidUrl(localUrl) ? localUrl : viewProps.url; + if (to >= editor.state.doc.content.size) return; + editor.view.dispatch(editor.state.tr.removeMark(from, to, editor.schema.marks.link)); editor.view.dispatch(editor.state.tr.addMark(from, to, editor.schema.marks.link.create({ href: url }))); }, - [localUrl] + [localUrl, editor, from, to, viewProps.url] ); const handleUpdateText = (text: string) => { From af70722e34ceeb09cf2c1bee04a69ce89e7d7791 Mon Sep 17 00:00:00 2001 From: Henit Chobisa Date: Tue, 5 Mar 2024 13:02:13 +0530 Subject: [PATCH 026/308] chore: added workflow for checking version before merge to master (#3847) --- .github/workflows/check-version.yml | 45 +++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/workflows/check-version.yml diff --git a/.github/workflows/check-version.yml b/.github/workflows/check-version.yml new file mode 100644 index 000000000..ca8b6f8b3 --- /dev/null +++ b/.github/workflows/check-version.yml @@ -0,0 +1,45 @@ +name: Version Change Before Release + +on: + pull_request: + branches: + - master + +jobs: + check-version: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Get PR Branch version + run: echo "PR_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV + + - name: Fetch base branch + run: git fetch origin master:master + + - name: Get Master Branch version + run: | + git checkout master + echo "MASTER_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV + + - name: Get master branch version and compare + run: | + echo "Comparing versions: PR version is $PR_VERSION, Master version is $MASTER_VERSION" + if [ "$PR_VERSION" == "$MASTER_VERSION" ]; then + echo "Version in PR branch is the same as in master. Failing the CI." + exit 1 + else + echo "Version check passed. Versions are different." + fi + env: + PR_VERSION: ${{ env.PR_VERSION }} + MASTER_VERSION: ${{ env.MASTER_VERSION }} From f8f9dd33311686e94c2386cfe94af14569662d90 Mon Sep 17 00:00:00 2001 From: Henit Chobisa Date: Tue, 5 Mar 2024 13:14:00 +0530 Subject: [PATCH 027/308] [CHANG-8] chore: Upgraded Build Pull Request CI for Faster Parallel Build with Linting Capabilities (#3838) * chore: upgraded build pull request ci for multi stage parallel builds * Update build-test-pull-request.yml --- .github/workflows/build-test-pull-request.yml | 111 +++++++++++++----- 1 file changed, 84 insertions(+), 27 deletions(-) diff --git a/.github/workflows/build-test-pull-request.yml b/.github/workflows/build-test-pull-request.yml index 83ed41625..e0014f696 100644 --- a/.github/workflows/build-test-pull-request.yml +++ b/.github/workflows/build-test-pull-request.yml @@ -1,27 +1,19 @@ -name: Build Pull Request Contents +name: Build and Lint on Pull Request on: + workflow_dispatch: pull_request: types: ["opened", "synchronize"] jobs: - build-pull-request-contents: - name: Build Pull Request Contents - runs-on: ubuntu-20.04 - permissions: - pull-requests: read - + get-changed-files: + runs-on: ubuntu-latest + outputs: + apiserver_changed: ${{ steps.changed-files.outputs.apiserver_any_changed }} + web_changed: ${{ steps.changed-files.outputs.web_any_changed }} + space_changed: ${{ steps.changed-files.outputs.deploy_any_changed }} steps: - - name: Checkout Repository to Actions - uses: actions/checkout@v3.3.0 - with: - token: ${{ secrets.ACCESS_TOKEN }} - - - name: Setup Node.js 18.x - uses: actions/setup-node@v2 - with: - node-version: 18.x - + - uses: actions/checkout@v3 - name: Get changed files id: changed-files uses: tj-actions/changed-files@v41 @@ -31,17 +23,82 @@ jobs: - apiserver/** web: - web/** + - packages/** + - 'package.json' + - 'yarn.lock' + - 'tsconfig.json' + - 'turbo.json' deploy: - space/** + - packages/** + - 'package.json' + - 'yarn.lock' + - 'tsconfig.json' + - 'turbo.json' - - name: Build Plane's Main App - if: steps.changed-files.outputs.web_any_changed == 'true' - run: | - yarn - yarn build --filter=web + lint-apiserver: + needs: get-changed-files + runs-on: ubuntu-latest + if: needs.get-changed-files.outputs.apiserver_changed == 'true' + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' # Specify the Python version you need + - name: Install Pylint + run: python -m pip install ruff + - name: Install Apiserver Dependencies + run: cd apiserver && pip install -r requirements.txt + - name: Lint apiserver + run: ruff check --fix apiserver - - name: Build Plane's Deploy App - if: steps.changed-files.outputs.deploy_any_changed == 'true' - run: | - yarn - yarn build --filter=space + lint-web: + needs: get-changed-files + if: needs.get-changed-files.outputs.web_changed == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: 18.x + - run: yarn install + - run: yarn lint --filter=web + + lint-space: + needs: get-changed-files + if: needs.get-changed-files.outputs.space_changed == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: 18.x + - run: yarn install + - run: yarn lint --filter=space + + build-web: + needs: lint-web + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: 18.x + - run: yarn install + - run: yarn build --filter=web + + build-space: + needs: lint-space + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: 18.x + - run: yarn install + - run: yarn build --filter=space From d07dd650222706aaf305b470bf2f6fee0c178b21 Mon Sep 17 00:00:00 2001 From: Manish Gupta <59428681+mguptahub@users.noreply.github.com> Date: Wed, 6 Mar 2024 12:57:14 +0530 Subject: [PATCH 028/308] feat: feature preview deploys for web and space nextjs applications (#3881) * feature preview deploy * chore: variable name changes --------- Co-authored-by: sriram veeraghanta --- .github/workflows/feature-deployment.yml | 73 ++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 .github/workflows/feature-deployment.yml diff --git a/.github/workflows/feature-deployment.yml b/.github/workflows/feature-deployment.yml new file mode 100644 index 000000000..2220a7a84 --- /dev/null +++ b/.github/workflows/feature-deployment.yml @@ -0,0 +1,73 @@ +name: Feature Preview + +on: + workflow_dispatch: + inputs: + web-build: + required: true + type: boolean + default: true + space-build: + required: true + type: boolean + default: false + +jobs: + feature-deploy: + name: Feature Deploy + runs-on: ubuntu-latest + env: + KUBE_CONFIG_FILE: ${{ secrets.KUBE_CONFIG }} + BUILD_WEB: ${{ (github.event.inputs.web-build == '' && true) || github.event.inputs.web-build }} + BUILD_SPACE: ${{ (github.event.inputs.space-build == '' && false) || github.event.inputs.space-build }} + + steps: + - name: Tailscale + uses: tailscale/github-action@v2 + with: + oauth-client-id: ${{ secrets.TAILSCALE_OAUTH_CLIENT_ID }} + oauth-secret: ${{ secrets.TAILSCALE_OAUTH_SECRET }} + tags: tag:ci + + - name: Kubectl Setup + run: | + curl -LO "https://dl.k8s.io/release/${{secrets.KUBE_VERSION}}/bin/linux/amd64/kubectl" + chmod +x kubectl + + mkdir -p ~/.kube + echo "$KUBE_CONFIG_FILE" > ~/.kube/config + chmod 600 ~/.kube/config + + - name: HELM Setup + run: | + curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 + chmod 700 get_helm.sh + ./get_helm.sh + + - name: App Deploy + run: | + helm --kube-insecure-skip-tls-verify repo add feature-preview ${{ secrets.FEATURE_PREVIEW_HELM_CHART_URL }} + GIT_BRANCH=${{ github.ref_name }} + APP_NAMESPACE=${{ secrets.FEATURE_PREVIEW_NAMESPACE }} + + METADATA=$(helm install feature-preview/${{ secrets.FEATURE_PREVIEW_HELM_CHART_NAME }} \ + --kube-insecure-skip-tls-verify \ + --generate-name \ + --namespace $APP_NAMESPACE \ + --set shared_config.git_repo=${{ github.repositoryUrl }} \ + --set shared_config.git_branch="$GIT_BRANCH" \ + --set web.enabled=${{ env.BUILD_WEB }} \ + --set space.enabled=${{ env.BUILD_SPACE }} \ + --output json \ + --timeout 1000s) + + APP_NAME=$(echo $METADATA | jq -r '.name') + + INGRESS_HOSTNAME=$(kubectl get ingress -n feature-builds --insecure-skip-tls-verify \ + -o jsonpath='{.items[?(@.metadata.annotations.meta\.helm\.sh\/release-name=="'$APP_NAME'")]}' | \ + jq -r '.spec.rules[0].host') + + echo "****************************************" + echo "APP NAME ::: $APP_NAME" + echo "INGRESS HOSTNAME ::: $INGRESS_HOSTNAME" + echo "****************************************" From 4d0f641ee0cae4fac344c6caaa44abc602442369 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Wed, 6 Mar 2024 14:02:14 +0530 Subject: [PATCH 029/308] [WEB-588] chore: remove the word `title` from the issue title tooltip. (#3874) * [WEB-588] chore: remove the word `title` from the issue title tooltip. * fix: github url fixes in feature deploy action --------- Co-authored-by: sriram veeraghanta --- .github/workflows/feature-deployment.yml | 2 +- web/components/cycles/active-cycle-details.tsx | 2 +- web/components/issues/issue-layouts/calendar/issue-blocks.tsx | 2 +- web/components/issues/issue-layouts/gantt/blocks.tsx | 2 +- web/components/issues/issue-layouts/kanban/block.tsx | 4 ++-- web/components/issues/issue-layouts/list/block.tsx | 4 ++-- web/components/issues/issue-layouts/spreadsheet/issue-row.tsx | 4 ++-- web/components/issues/sub-issues/issue-list-item.tsx | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/feature-deployment.yml b/.github/workflows/feature-deployment.yml index 2220a7a84..7b9f5ffcc 100644 --- a/.github/workflows/feature-deployment.yml +++ b/.github/workflows/feature-deployment.yml @@ -54,7 +54,7 @@ jobs: --kube-insecure-skip-tls-verify \ --generate-name \ --namespace $APP_NAMESPACE \ - --set shared_config.git_repo=${{ github.repositoryUrl }} \ + --set shared_config.git_repo=${{github.server_url}}/${{ github.repository }}.git \ --set shared_config.git_branch="$GIT_BRANCH" \ --set web.enabled=${{ env.BUILD_WEB }} \ --set space.enabled=${{ env.BUILD_SPACE }} \ diff --git a/web/components/cycles/active-cycle-details.tsx b/web/components/cycles/active-cycle-details.tsx index 1fae0412f..425ce7df3 100644 --- a/web/components/cycles/active-cycle-details.tsx +++ b/web/components/cycles/active-cycle-details.tsx @@ -311,7 +311,7 @@ export const ActiveCycleDetails: React.FC = observer((props {currentProjectDetails?.identifier}-{issue.sequence_id} - + {truncateText(issue.name, 30)}
diff --git a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx index b5d0c4346..ac6005372 100644 --- a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx +++ b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx @@ -110,7 +110,7 @@ export const CalendarIssueBlocks: React.FC = observer((props) => {
{getProjectIdentifierById(issue?.project_id)}-{issue.sequence_id}
- +
{issue.name}
diff --git a/web/components/issues/issue-layouts/gantt/blocks.tsx b/web/components/issues/issue-layouts/gantt/blocks.tsx index 209d876ac..730313549 100644 --- a/web/components/issues/issue-layouts/gantt/blocks.tsx +++ b/web/components/issues/issue-layouts/gantt/blocks.tsx @@ -97,7 +97,7 @@ export const IssueGanttSidebarBlock: React.FC = observer((props) => {
{projectIdentifier} {issueDetails?.sequence_id}
- + {issueDetails?.name}
diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index be27f7706..602c6a934 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -71,7 +71,7 @@ const KanbanIssueDetailsBlock: React.FC = observer((prop {issue?.is_draft ? ( - + {issue.name} ) : ( @@ -84,7 +84,7 @@ const KanbanIssueDetailsBlock: React.FC = observer((prop className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" disabled={!!issue?.tempId} > - + {issue.name} diff --git a/web/components/issues/issue-layouts/list/block.tsx b/web/components/issues/issue-layouts/list/block.tsx index cc04ed716..90fee10cc 100644 --- a/web/components/issues/issue-layouts/list/block.tsx +++ b/web/components/issues/issue-layouts/list/block.tsx @@ -65,7 +65,7 @@ export const IssueBlock: React.FC = observer((props: IssueBlock )} {issue?.is_draft ? ( - + {issue.name} ) : ( @@ -78,7 +78,7 @@ export const IssueBlock: React.FC = observer((props: IssueBlock className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" disabled={!!issue?.tempId} > - + {issue.name} diff --git a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx index 9f4810c78..abf6c3a01 100644 --- a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx @@ -241,9 +241,9 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => { disabled={!!issueDetail?.tempId} >
- +
{issueDetail.name} diff --git a/web/components/issues/sub-issues/issue-list-item.tsx b/web/components/issues/sub-issues/issue-list-item.tsx index c6b87411d..a748e986e 100644 --- a/web/components/issues/sub-issues/issue-list-item.tsx +++ b/web/components/issues/sub-issues/issue-list-item.tsx @@ -117,7 +117,7 @@ export const IssueListItem: React.FC = observer((props) => { onClick={() => handleIssuePeekOverview(issue)} className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" > - + {issue.name} From c06ef4d1d77942d68a7f082f119712535312429a Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:18:19 +0530 Subject: [PATCH 030/308] [WEB-579] style: scrollbar implementation (#3835) * style: scrollbar added in profile summary and sidebar * style: scrollbar added in modals * style: scrollbar added in project setting screens * style: scrollbar added in workspace and profile settings * style: scrollbar added in dropdowns and filters --- .../analytics/custom-analytics/main-content.tsx | 2 +- .../custom-analytics/sidebar/projects-list.tsx | 4 ++-- .../analytics/custom-analytics/sidebar/sidebar.tsx | 2 +- web/components/core/image-picker-popover.tsx | 2 +- .../core/modals/existing-issues-list-modal.tsx | 2 +- web/components/emoji-icon-picker/index.tsx | 2 +- .../display-filters/display-filters-selection.tsx | 2 +- .../filters/header/filters/filters-selection.tsx | 2 +- web/components/issues/parent-issues-list-modal.tsx | 5 ++++- web/components/profile/sidebar.tsx | 11 ++++++----- web/layouts/settings-layout/profile/layout.tsx | 4 +++- .../settings-layout/profile/preferences/layout.tsx | 4 +++- web/layouts/settings-layout/profile/sidebar.tsx | 4 ++-- web/layouts/settings-layout/project/layout.tsx | 4 +++- web/layouts/settings-layout/workspace/layout.tsx | 4 ++-- web/pages/[workspaceSlug]/profile/[userId]/index.tsx | 2 +- .../projects/[projectId]/settings/estimates.tsx | 2 +- .../projects/[projectId]/settings/integrations.tsx | 2 +- .../projects/[projectId]/settings/labels.tsx | 2 +- web/pages/[workspaceSlug]/settings/api-tokens.tsx | 2 +- web/pages/[workspaceSlug]/settings/webhooks/index.tsx | 2 +- web/pages/profile/activity.tsx | 2 +- web/pages/profile/index.tsx | 2 +- web/pages/profile/preferences/email.tsx | 2 +- 24 files changed, 41 insertions(+), 31 deletions(-) diff --git a/web/components/analytics/custom-analytics/main-content.tsx b/web/components/analytics/custom-analytics/main-content.tsx index 3c199f807..7781e7869 100644 --- a/web/components/analytics/custom-analytics/main-content.tsx +++ b/web/components/analytics/custom-analytics/main-content.tsx @@ -33,7 +33,7 @@ export const CustomAnalyticsMainContent: React.FC = (props) => { {!error ? ( analytics ? ( analytics.total > 0 ? ( -
+
= observer((pro return (

Selected Projects

-
+
{projectIds.map((projectId) => { const project = getProjectById(projectId); @@ -42,7 +42,7 @@ export const CustomAnalyticsSidebarProjectsList: React.FC = observer((pro ({project.identifier})
-
+
diff --git a/web/components/analytics/custom-analytics/sidebar/sidebar.tsx b/web/components/analytics/custom-analytics/sidebar/sidebar.tsx index 3ad2805f2..bf1c80fea 100644 --- a/web/components/analytics/custom-analytics/sidebar/sidebar.tsx +++ b/web/components/analytics/custom-analytics/sidebar/sidebar.tsx @@ -1,4 +1,4 @@ -import { useEffect, } from "react"; +import { useEffect } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { mutate } from "swr"; diff --git a/web/components/core/image-picker-popover.tsx b/web/components/core/image-picker-popover.tsx index b2e4c4c9f..09a1fd4e4 100644 --- a/web/components/core/image-picker-popover.tsx +++ b/web/components/core/image-picker-popover.tsx @@ -187,7 +187,7 @@ export const ImagePickerPopover: React.FC = observer((props) => { ); })} - + {(unsplashImages || !unsplashError) && (
diff --git a/web/components/core/modals/existing-issues-list-modal.tsx b/web/components/core/modals/existing-issues-list-modal.tsx index c4fa25c6d..1b6a1e76b 100644 --- a/web/components/core/modals/existing-issues-list-modal.tsx +++ b/web/components/core/modals/existing-issues-list-modal.tsx @@ -184,7 +184,7 @@ export const ExistingIssuesListModal: React.FC = (props) => { )}
- + {searchTerm !== "" && (
Search results for{" "} diff --git a/web/components/emoji-icon-picker/index.tsx b/web/components/emoji-icon-picker/index.tsx index 57d5d8896..9c45e5356 100644 --- a/web/components/emoji-icon-picker/index.tsx +++ b/web/components/emoji-icon-picker/index.tsx @@ -94,7 +94,7 @@ const EmojiIconPicker: React.FC = (props) => { ))} - + {recentEmojis.length > 0 && (
diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx index 131bea46b..b8988580a 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx @@ -37,7 +37,7 @@ export const DisplayFiltersSelection: React.FC = observer((props) => { Object.keys(layoutDisplayFiltersOptions?.display_filters ?? {}).includes(displayFilter); return ( -
+
{/* display properties */} {layoutDisplayFiltersOptions?.display_properties && (
diff --git a/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx b/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx index afdee86f2..ae7ded8b2 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx @@ -63,7 +63,7 @@ export const FilterSelection: React.FC = observer((props) => { )}
-
+
{/* priority */} {isFilterEnabled("priority") && (
diff --git a/web/components/issues/parent-issues-list-modal.tsx b/web/components/issues/parent-issues-list-modal.tsx index c8520562e..b97eafc06 100644 --- a/web/components/issues/parent-issues-list-modal.tsx +++ b/web/components/issues/parent-issues-list-modal.tsx @@ -136,7 +136,10 @@ export const ParentIssuesListModal: React.FC = ({
- + {searchTerm !== "" && (
Search results for{" "} diff --git a/web/components/profile/sidebar.tsx b/web/components/profile/sidebar.tsx index 107c1f528..71d935d3c 100644 --- a/web/components/profile/sidebar.tsx +++ b/web/components/profile/sidebar.tsx @@ -76,7 +76,7 @@ export const ProfileSidebar = observer(() => { return (
{userProjectsData ? ( @@ -162,12 +162,13 @@ export const ProfileSidebar = observer(() => { {project.assigned_issues > 0 && (
{completedIssuePercentage}%
diff --git a/web/layouts/settings-layout/profile/layout.tsx b/web/layouts/settings-layout/profile/layout.tsx index 08dfd5509..5bf5f0eea 100644 --- a/web/layouts/settings-layout/profile/layout.tsx +++ b/web/layouts/settings-layout/profile/layout.tsx @@ -21,7 +21,9 @@ export const ProfileSettingsLayout: FC = (props) => {
{header} -
{children}
+
+ {children} +
diff --git a/web/layouts/settings-layout/profile/preferences/layout.tsx b/web/layouts/settings-layout/profile/preferences/layout.tsx index 0e1d31587..116813958 100644 --- a/web/layouts/settings-layout/profile/preferences/layout.tsx +++ b/web/layouts/settings-layout/profile/preferences/layout.tsx @@ -73,7 +73,9 @@ export const ProfilePreferenceSettingsLayout: FC
{header} -
{children}
+
+ {children} +
diff --git a/web/layouts/settings-layout/profile/sidebar.tsx b/web/layouts/settings-layout/profile/sidebar.tsx index 85f82961f..3e515cc64 100644 --- a/web/layouts/settings-layout/profile/sidebar.tsx +++ b/web/layouts/settings-layout/profile/sidebar.tsx @@ -129,7 +129,7 @@ export const ProfileLayoutSidebar = observer(() => { {!sidebarCollapsed && (
Your account
)} -
+
{PROFILE_ACTION_LINKS.map((link) => { if (link.key === "change-password" && currentUser?.is_password_autoset) return null; @@ -157,7 +157,7 @@ export const ProfileLayoutSidebar = observer(() => {
Workspaces
)} {workspacesList && workspacesList.length > 0 && ( -
+
{workspacesList.map((workspace) => ( = observer((props)
-
{children}
+
+ {children} +
); }); diff --git a/web/layouts/settings-layout/workspace/layout.tsx b/web/layouts/settings-layout/workspace/layout.tsx index 4ee0f1e33..3d5d057be 100644 --- a/web/layouts/settings-layout/workspace/layout.tsx +++ b/web/layouts/settings-layout/workspace/layout.tsx @@ -10,11 +10,11 @@ export const WorkspaceSettingLayout: FC = (props) => { const { children } = props; return ( -
+
-
+
{children}
diff --git a/web/pages/[workspaceSlug]/profile/[userId]/index.tsx b/web/pages/[workspaceSlug]/profile/[userId]/index.tsx index a4d1debe1..7d24a8b11 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/index.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/index.tsx @@ -45,7 +45,7 @@ const ProfileOverviewPage: NextPageWithLayout = () => { return ( <> -
+
diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx index 3aea45adb..70108f90a 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx @@ -26,7 +26,7 @@ const EstimatesSettingsPage: NextPageWithLayout = observer(() => { return ( <> -
+
diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx index 06246f1c2..5c9faae7c 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx @@ -56,7 +56,7 @@ const ProjectIntegrationsPage: NextPageWithLayout = observer(() => { return ( <> -
+

Integrations

diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx index 3bb1c8c04..d62ac1e66 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx @@ -19,7 +19,7 @@ const LabelsSettingsPage: NextPageWithLayout = observer(() => { return ( <> -
+
diff --git a/web/pages/[workspaceSlug]/settings/api-tokens.tsx b/web/pages/[workspaceSlug]/settings/api-tokens.tsx index 1f203ff04..35366cb0a 100644 --- a/web/pages/[workspaceSlug]/settings/api-tokens.tsx +++ b/web/pages/[workspaceSlug]/settings/api-tokens.tsx @@ -71,7 +71,7 @@ const ApiTokensPage: NextPageWithLayout = observer(() => { <> setIsCreateTokenModalOpen(false)} /> -
+
{tokens.length > 0 ? ( <>
diff --git a/web/pages/[workspaceSlug]/settings/webhooks/index.tsx b/web/pages/[workspaceSlug]/settings/webhooks/index.tsx index 46c7e99cb..19f23913e 100644 --- a/web/pages/[workspaceSlug]/settings/webhooks/index.tsx +++ b/web/pages/[workspaceSlug]/settings/webhooks/index.tsx @@ -70,7 +70,7 @@ const WebhooksListPage: NextPageWithLayout = observer(() => { return ( <> -
+
{

Activity

{userActivity ? ( -
+
    {userActivity.results.map((activityItem: any) => { if (activityItem.field === "comment") { diff --git a/web/pages/profile/index.tsx b/web/pages/profile/index.tsx index bdde41d08..c4eab324a 100644 --- a/web/pages/profile/index.tsx +++ b/web/pages/profile/index.tsx @@ -163,7 +163,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { )} /> setDeactivateAccountModal(false)} /> -
    +
    diff --git a/web/pages/profile/preferences/email.tsx b/web/pages/profile/preferences/email.tsx index 34bd6fb03..ddd23abdf 100644 --- a/web/pages/profile/preferences/email.tsx +++ b/web/pages/profile/preferences/email.tsx @@ -28,7 +28,7 @@ const ProfilePreferencesThemePage: NextPageWithLayout = () => { return ( <> -
    +
    From 53367a6bc4cb4fbd49ede6582b7274b41d0ca278 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Wed, 6 Mar 2024 14:18:41 +0530 Subject: [PATCH 031/308] [WEB-570] chore: toast refactor (#3836) * new toast setup * chore: new toast implementation. * chore: move toast component to ui package. * chore: replace `setToast` with `setPromiseToast` in required places for better UX. * chore: code cleanup. * chore: update theme. * fix: theme switching issue. * chore: remove toast from issue update operations. * chore: add promise toast for add/ remove issue to cycle/ module and remove local spinners. --------- Co-authored-by: rahulramesha --- .../tailwind-config-custom/tailwind.config.js | 25 +++ packages/ui/package.json | 1 + packages/ui/src/index.ts | 1 + .../ui/src/spinners/circular-bar-spinner.tsx | 35 +++ packages/ui/src/spinners/index.ts | 1 + packages/ui/src/toast/index.tsx | 206 +++++++++++++++++ .../account/deactivate-account-modal.tsx | 18 +- .../account/o-auth/o-auth-options.tsx | 13 +- .../account/sign-in-forms/email.tsx | 9 +- .../sign-in-forms/optional-set-password.tsx | 13 +- .../account/sign-in-forms/password.tsx | 13 +- .../account/sign-in-forms/unique-code.tsx | 17 +- .../account/sign-up-forms/email.tsx | 9 +- .../sign-up-forms/optional-set-password.tsx | 16 +- .../account/sign-up-forms/password.tsx | 10 +- .../account/sign-up-forms/unique-code.tsx | 17 +- .../custom-analytics/sidebar/sidebar.tsx | 13 +- .../api-token/delete-token-modal.tsx | 14 +- .../api-token/modal/create-token-modal.tsx | 12 +- web/components/api-token/modal/form.tsx | 10 +- .../modal/generated-token-details.tsx | 10 +- .../actions/issue-actions/actions-list.tsx | 14 +- .../command-palette/actions/theme-actions.tsx | 8 +- .../command-palette/command-palette.tsx | 15 +- .../core/modals/bulk-delete-issues-modal.tsx | 18 +- .../modals/existing-issues-list-modal.tsx | 14 +- .../core/modals/gpt-assistant-popover.tsx | 13 +- .../core/modals/user-image-upload-modal.tsx | 10 +- .../modals/workspace-image-upload-modal.tsx | 10 +- web/components/core/sidebar/links-list.tsx | 11 +- .../cycles/active-cycle-details.tsx | 40 ++-- web/components/cycles/cycles-board-card.tsx | 81 ++++--- web/components/cycles/cycles-list-item.tsx | 81 ++++--- web/components/cycles/delete-modal.tsx | 13 +- web/components/cycles/modal.tsx | 25 +-- web/components/cycles/sidebar.tsx | 21 +- .../cycles/transfer-issues-modal.tsx | 14 +- .../create-update-estimate-modal.tsx | 29 ++- .../estimates/delete-estimate-modal.tsx | 9 +- .../estimates/estimate-list-item.tsx | 9 +- web/components/estimates/estimates-list.tsx | 9 +- web/components/exporter/export-modal.tsx | 14 +- web/components/inbox/inbox-issue-actions.tsx | 13 +- .../inbox/modals/create-issue-modal.tsx | 20 +- .../inbox/modals/select-duplicate.tsx | 11 +- web/components/instance/ai-form.tsx | 9 +- web/components/instance/email-form.tsx | 9 +- web/components/instance/general-form.tsx | 9 +- .../instance/github-config-form.tsx | 13 +- .../instance/google-config-form.tsx | 13 +- web/components/instance/image-config-form.tsx | 9 +- .../instance/setup-form/sign-in-form.tsx | 10 +- web/components/instance/sidebar-dropdown.tsx | 9 +- .../integration/delete-import-modal.tsx | 10 +- web/components/integration/github/root.tsx | 10 +- .../integration/single-integration-card.tsx | 13 +- web/components/issues/archive-issue-modal.tsx | 9 +- web/components/issues/attachment/root.tsx | 38 ++-- web/components/issues/delete-issue-modal.tsx | 9 +- web/components/issues/description-form.tsx | 14 +- web/components/issues/description-input.tsx | 8 +- .../issues/issue-detail/cycle-select.tsx | 1 - .../issues/issue-detail/inbox/root.tsx | 26 +-- .../issue-detail/issue-activity/root.tsx | 30 +-- .../issue-detail/label/create-label.tsx | 8 +- .../issues/issue-detail/label/root.tsx | 24 +- .../issues/issue-detail/links/link-detail.tsx | 8 +- .../issues/issue-detail/links/root.tsx | 31 ++- .../issues/issue-detail/module-select.tsx | 1 - .../issue-detail/reactions/issue-comment.tsx | 21 +- .../issues/issue-detail/reactions/issue.tsx | 22 +- .../issues/issue-detail/relation-select.tsx | 9 +- web/components/issues/issue-detail/root.tsx | 161 ++++++-------- .../issues/issue-detail/sidebar.tsx | 20 +- .../issues/issue-detail/subscription.tsx | 12 +- .../calendar/base-calendar-root.tsx | 9 +- .../calendar/quick-add-issue-form.tsx | 70 +++--- .../issue-layouts/empty-states/cycle.tsx | 9 +- .../issue-layouts/empty-states/module.tsx | 9 +- .../gantt/quick-add-issue-form.tsx | 48 ++-- .../issue-layouts/kanban/base-kanban-root.tsx | 9 +- .../kanban/headers/group-by-card.tsx | 10 +- .../kanban/quick-add-issue-form.tsx | 63 +++--- .../list/headers/group-by-card.tsx | 10 +- .../list/quick-add-issue-form.tsx | 52 +++-- .../quick-action-dropdowns/all-issue.tsx | 12 +- .../quick-action-dropdowns/archived-issue.tsx | 10 +- .../quick-action-dropdowns/cycle-issue.tsx | 15 +- .../quick-action-dropdowns/module-issue.tsx | 14 +- .../quick-action-dropdowns/project-issue.tsx | 14 +- .../spreadsheet/quick-add-issue-form.tsx | 79 ++++--- .../issues/issue-modal/draft-issue-layout.tsx | 13 +- web/components/issues/issue-modal/form.tsx | 17 +- web/components/issues/issue-modal/modal.tsx | 22 +- .../issues/peek-overview/header.tsx | 19 +- web/components/issues/peek-overview/root.tsx | 210 +++++++++--------- web/components/issues/peek-overview/view.tsx | 5 +- web/components/issues/sub-issues/root.tsx | 47 ++-- web/components/issues/title-input.tsx | 2 +- web/components/labels/create-label-modal.tsx | 9 +- .../labels/create-update-label-inline.tsx | 13 +- web/components/labels/delete-label-modal.tsx | 10 +- .../modules/delete-module-modal.tsx | 13 +- web/components/modules/modal.tsx | 21 +- web/components/modules/module-card-item.tsx | 71 +++--- web/components/modules/module-list-item.tsx | 80 ++++--- web/components/modules/sidebar.tsx | 50 +++-- .../notifications/notification-card.tsx | 29 ++- .../select-snooze-till-modal.tsx | 10 +- web/components/onboarding/invite-members.tsx | 12 +- .../switch-delete-account-modal.tsx | 17 +- web/components/onboarding/workspace.tsx | 17 +- web/components/pages/delete-page-modal.tsx | 14 +- .../preferences/email-notification-form.tsx | 10 +- web/components/project/card.tsx | 39 ++-- .../project/create-project-modal.tsx | 25 +-- .../project/delete-project-modal.tsx | 13 +- web/components/project/form.tsx | 13 +- web/components/project/integration-card.tsx | 13 +- .../project/leave-project-modal.tsx | 21 +- web/components/project/member-list-item.tsx | 17 +- .../project-settings-member-defaults.tsx | 9 +- .../project/publish-project/modal.tsx | 17 +- .../project/send-project-invitation-modal.tsx | 9 +- .../project/settings/features-list.tsx | 10 +- web/components/project/sidebar-list-item.tsx | 45 ++-- web/components/project/sidebar-list.tsx | 13 +- web/components/states/create-state-modal.tsx | 13 +- .../states/create-update-state-inline.tsx | 29 ++- web/components/states/delete-state-modal.tsx | 13 +- web/components/toast-alert/index.tsx | 61 ----- web/components/views/delete-view-modal.tsx | 13 +- web/components/views/modal.tsx | 17 +- web/components/views/view-list-item.tsx | 9 +- .../web-hooks/create-webhook-modal.tsx | 13 +- .../web-hooks/delete-webhook-modal.tsx | 13 +- web/components/web-hooks/form/secret-key.tsx | 22 +- .../workspace/create-workspace-form.tsx | 17 +- .../workspace/delete-workspace-modal.tsx | 13 +- .../settings/invitations-list-item.tsx | 17 +- .../workspace/settings/members-list-item.tsx | 17 +- .../workspace/settings/workspace-details.tsx | 21 +- web/components/workspace/sidebar-dropdown.tsx | 10 +- .../workspace/views/delete-view-modal.tsx | 9 +- web/components/workspace/views/modal.tsx | 21 +- web/contexts/toast.context.tsx | 97 -------- web/helpers/theme.helper.ts | 3 + web/hooks/use-toast.tsx | 9 - web/hooks/use-user-notifications.tsx | 14 +- .../settings-layout/profile/sidebar.tsx | 9 +- web/lib/app-provider.tsx | 53 ++--- .../archived-issues/[archivedIssueId].tsx | 12 +- .../projects/[projectId]/pages/[pageId].tsx | 9 +- .../[projectId]/settings/automations.tsx | 10 +- .../[workspaceSlug]/settings/members.tsx | 13 +- .../settings/webhooks/[webhookId].tsx | 14 +- web/pages/_app.tsx | 6 +- web/pages/_error.tsx | 10 +- web/pages/accounts/forgot-password.tsx | 13 +- web/pages/accounts/reset-password.tsx | 9 +- web/pages/god-mode/authorization.tsx | 15 +- web/pages/invitations/index.tsx | 17 +- web/pages/profile/change-password.tsx | 37 ++- web/pages/profile/index.tsx | 58 ++--- web/pages/profile/preferences/theme.tsx | 21 +- web/styles/globals.css | 42 ++++ yarn.lock | 5 + 167 files changed, 1827 insertions(+), 1896 deletions(-) create mode 100644 packages/ui/src/spinners/circular-bar-spinner.tsx create mode 100644 packages/ui/src/toast/index.tsx delete mode 100644 web/components/toast-alert/index.tsx delete mode 100644 web/contexts/toast.context.tsx delete mode 100644 web/hooks/use-toast.tsx diff --git a/packages/tailwind-config-custom/tailwind.config.js b/packages/tailwind-config-custom/tailwind.config.js index 3465b8196..5d767e84f 100644 --- a/packages/tailwind-config-custom/tailwind.config.js +++ b/packages/tailwind-config-custom/tailwind.config.js @@ -198,6 +198,31 @@ module.exports = { 300: convertToRGB("--color-onboarding-border-300"), }, }, + toast: { + text: { + success: convertToRGB("--color-toast-success-text"), + error: convertToRGB("--color-toast-error-text"), + warning: convertToRGB("--color-toast-warning-text"), + info: convertToRGB("--color-toast-info-text"), + loading: convertToRGB("--color-toast-loading-text"), + secondary: convertToRGB("--color-toast-secondary-text"), + tertiary: convertToRGB("--color-toast-tertiary-text"), + }, + background: { + success: convertToRGB("--color-toast-success-background"), + error: convertToRGB("--color-toast-error-background"), + warning: convertToRGB("--color-toast-warning-background"), + info: convertToRGB("--color-toast-info-background"), + loading: convertToRGB("--color-toast-loading-background"), + }, + border: { + success: convertToRGB("--color-toast-success-border"), + error: convertToRGB("--color-toast-error-border"), + warning: convertToRGB("--color-toast-warning-border"), + info: convertToRGB("--color-toast-info-border"), + loading: convertToRGB("--color-toast-loading-border"), + }, + }, }, keyframes: { leftToaster: { diff --git a/packages/ui/package.json b/packages/ui/package.json index 756a0f2f1..91a010a1e 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -26,6 +26,7 @@ "react-color": "^2.19.3", "react-dom": "^18.2.0", "react-popper": "^2.3.0", + "sonner": "^1.4.2", "tailwind-merge": "^2.0.0" }, "devDependencies": { diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index b90b6993a..218d375fa 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -10,3 +10,4 @@ export * from "./spinners"; export * from "./tooltip"; export * from "./loader"; export * from "./control-link"; +export * from "./toast"; diff --git a/packages/ui/src/spinners/circular-bar-spinner.tsx b/packages/ui/src/spinners/circular-bar-spinner.tsx new file mode 100644 index 000000000..3be8af43a --- /dev/null +++ b/packages/ui/src/spinners/circular-bar-spinner.tsx @@ -0,0 +1,35 @@ +import * as React from "react"; + +interface ICircularBarSpinner extends React.SVGAttributes { + height?: string; + width?: string; + className?: string | undefined; +} + +export const CircularBarSpinner: React.FC = ({ + height = "16px", + width = "16px", + className = "", +}) => ( +
    + + + + + + + + + + + + +
    +); diff --git a/packages/ui/src/spinners/index.ts b/packages/ui/src/spinners/index.ts index 768568172..a871a9b77 100644 --- a/packages/ui/src/spinners/index.ts +++ b/packages/ui/src/spinners/index.ts @@ -1 +1,2 @@ export * from "./circular-spinner"; +export * from "./circular-bar-spinner"; diff --git a/packages/ui/src/toast/index.tsx b/packages/ui/src/toast/index.tsx new file mode 100644 index 000000000..755326275 --- /dev/null +++ b/packages/ui/src/toast/index.tsx @@ -0,0 +1,206 @@ +import * as React from "react"; +import { Toaster, toast } from "sonner"; +// icons +import { AlertTriangle, CheckCircle2, X, XCircle } from "lucide-react"; +// spinner +import { CircularBarSpinner } from "../spinners"; +// helper +import { cn } from "../../helpers"; + +export enum TOAST_TYPE { + SUCCESS = "success", + ERROR = "error", + INFO = "info", + WARNING = "warning", + LOADING = "loading", +} + +type SetToastProps = + | { + type: TOAST_TYPE.LOADING; + title?: string; + } + | { + id?: string | number; + type: Exclude; + title: string; + message?: string; + }; + +type PromiseToastCallback = (data: ToastData) => string; + +type PromiseToastData = { + title: string; + message?: PromiseToastCallback; +}; + +type PromiseToastOptions = { + loading?: string; + success: PromiseToastData; + error: PromiseToastData; +}; + +type ToastContentProps = { + toastId: string | number; + icon?: React.ReactNode; + textColorClassName: string; + backgroundColorClassName: string; + borderColorClassName: string; +}; + +type ToastProps = { + theme: "light" | "dark" | "system"; +}; + +export const Toast = (props: ToastProps) => { + const { theme } = props; + return ; +}; + +export const setToast = (props: SetToastProps) => { + const renderToastContent = ({ + toastId, + icon, + textColorClassName, + backgroundColorClassName, + borderColorClassName, + }: ToastContentProps) => + props.type === TOAST_TYPE.LOADING ? ( +
    { + e.stopPropagation(); + e.preventDefault(); + }} + className={cn("w-[350px] h-[67.3px] rounded-lg border shadow-sm p-2", backgroundColorClassName, borderColorClassName)} + > +
    + {icon &&
    {icon}
    } +
    +
    {props.title ?? "Loading..."}
    +
    + toast.dismiss(toastId)} + /> +
    +
    +
    +
    + ) : ( +
    { + e.stopPropagation(); + e.preventDefault(); + }} + className={cn( + "relative flex flex-col w-[350px] rounded-lg border shadow-sm p-2", + backgroundColorClassName, + borderColorClassName + )} + > + toast.dismiss(toastId)} + /> +
    + {icon &&
    {icon}
    } +
    +
    {props.title}
    + {props.message &&
    {props.message}
    } +
    +
    +
    + ); + + switch (props.type) { + case TOAST_TYPE.SUCCESS: + return toast.custom( + (toastId) => + renderToastContent({ + toastId, + icon: , + textColorClassName: "text-toast-text-success", + backgroundColorClassName: "bg-toast-background-success", + borderColorClassName: "border-toast-border-success", + }), + props.id ? { id: props.id } : {} + ); + case TOAST_TYPE.ERROR: + return toast.custom( + (toastId) => + renderToastContent({ + toastId, + icon: , + textColorClassName: "text-toast-text-error", + backgroundColorClassName: "bg-toast-background-error", + borderColorClassName: "border-toast-border-error", + }), + props.id ? { id: props.id } : {} + ); + case TOAST_TYPE.WARNING: + return toast.custom( + (toastId) => + renderToastContent({ + toastId, + icon: , + textColorClassName: "text-toast-text-warning", + backgroundColorClassName: "bg-toast-background-warning", + borderColorClassName: "border-toast-border-warning", + }), + props.id ? { id: props.id } : {} + ); + case TOAST_TYPE.INFO: + return toast.custom( + (toastId) => + renderToastContent({ + toastId, + textColorClassName: "text-toast-text-info", + backgroundColorClassName: "bg-toast-background-info", + borderColorClassName: "border-toast-border-info", + }), + props.id ? { id: props.id } : {} + ); + + case TOAST_TYPE.LOADING: + return toast.custom((toastId) => + renderToastContent({ + toastId, + icon: , + textColorClassName: "text-toast-text-loading", + backgroundColorClassName: "bg-toast-background-loading", + borderColorClassName: "border-toast-border-loading", + }) + ); + } +}; + +export const setPromiseToast = ( + promise: Promise, + options: PromiseToastOptions +): void => { + const tId = setToast({ type: TOAST_TYPE.LOADING, title: options.loading }); + + promise + .then((data: ToastData) => { + setToast({ + type: TOAST_TYPE.SUCCESS, + id: tId, + title: options.success.title, + message: options.success.message?.(data), + }); + }) + .catch((data: ToastData) => { + setToast({ + type: TOAST_TYPE.ERROR, + id: tId, + title: options.error.title, + message: options.error.message?.(data), + }); + }); +}; diff --git a/web/components/account/deactivate-account-modal.tsx b/web/components/account/deactivate-account-modal.tsx index 701db6ad9..41d1fd7ca 100644 --- a/web/components/account/deactivate-account-modal.tsx +++ b/web/components/account/deactivate-account-modal.tsx @@ -7,9 +7,7 @@ import { mutate } from "swr"; // hooks import { useUser } from "hooks/store"; // ui -import { Button } from "@plane/ui"; -// hooks -import useToast from "hooks/use-toast"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; type Props = { isOpen: boolean; @@ -26,7 +24,6 @@ export const DeactivateAccountModal: React.FC = (props) => { const router = useRouter(); - const { setToastAlert } = useToast(); const { setTheme } = useTheme(); const handleClose = () => { @@ -39,8 +36,8 @@ export const DeactivateAccountModal: React.FC = (props) => { await deactivateAccount() .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Account deactivated successfully.", }); @@ -50,8 +47,8 @@ export const DeactivateAccountModal: React.FC = (props) => { handleClose(); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error, }) @@ -90,7 +87,10 @@ export const DeactivateAccountModal: React.FC = (props) => {
    -
    diff --git a/web/components/account/o-auth/o-auth-options.tsx b/web/components/account/o-auth/o-auth-options.tsx index 7c8468acb..bbb73b855 100644 --- a/web/components/account/o-auth/o-auth-options.tsx +++ b/web/components/account/o-auth/o-auth-options.tsx @@ -3,7 +3,8 @@ import { observer } from "mobx-react-lite"; import { AuthService } from "services/auth.service"; // hooks import { useApplication } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { GitHubSignInButton, GoogleSignInButton } from "components/account"; @@ -17,8 +18,6 @@ const authService = new AuthService(); export const OAuthOptions: React.FC = observer((props) => { const { handleSignInRedirection, type } = props; - // toast alert - const { setToastAlert } = useToast(); // mobx store const { config: { envConfig }, @@ -39,9 +38,9 @@ export const OAuthOptions: React.FC = observer((props) => { if (response) handleSignInRedirection(); } else throw Error("Cant find credentials"); } catch (err: any) { - setToastAlert({ + setToast({ + type: TOAST_TYPE.ERROR, title: "Error signing in!", - type: "error", message: err?.error || "Something went wrong. Please try again later or contact the support team.", }); } @@ -60,9 +59,9 @@ export const OAuthOptions: React.FC = observer((props) => { if (response) handleSignInRedirection(); } else throw Error("Cant find credentials"); } catch (err: any) { - setToastAlert({ + setToast({ + type: TOAST_TYPE.ERROR, title: "Error signing in!", - type: "error", message: err?.error || "Something went wrong. Please try again later or contact the support team.", }); } diff --git a/web/components/account/sign-in-forms/email.tsx b/web/components/account/sign-in-forms/email.tsx index 67ef720fe..881c75f83 100644 --- a/web/components/account/sign-in-forms/email.tsx +++ b/web/components/account/sign-in-forms/email.tsx @@ -4,10 +4,8 @@ import { XCircle } from "lucide-react"; import { observer } from "mobx-react-lite"; // services import { AuthService } from "services/auth.service"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // types @@ -27,7 +25,6 @@ const authService = new AuthService(); export const SignInEmailForm: React.FC = observer((props) => { const { onSubmit, updateEmail } = props; // hooks - const { setToastAlert } = useToast(); const { control, formState: { errors, isSubmitting, isValid }, @@ -52,8 +49,8 @@ export const SignInEmailForm: React.FC = observer((props) => { .emailCheck(payload) .then((res) => onSubmit(res.is_password_autoset)) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }) diff --git a/web/components/account/sign-in-forms/optional-set-password.tsx b/web/components/account/sign-in-forms/optional-set-password.tsx index 1ea5ca792..8fc7935cd 100644 --- a/web/components/account/sign-in-forms/optional-set-password.tsx +++ b/web/components/account/sign-in-forms/optional-set-password.tsx @@ -3,10 +3,9 @@ import { Controller, useForm } from "react-hook-form"; // services import { AuthService } from "services/auth.service"; // hooks -import useToast from "hooks/use-toast"; import { useEventTracker } from "hooks/store"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // icons @@ -38,8 +37,6 @@ export const SignInOptionalSetPasswordForm: React.FC = (props) => { const [showPassword, setShowPassword] = useState(false); // store hooks const { captureEvent } = useEventTracker(); - // toast alert - const { setToastAlert } = useToast(); // form info const { control, @@ -62,8 +59,8 @@ export const SignInOptionalSetPasswordForm: React.FC = (props) => { await authService .setPassword(payload) .then(async () => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Password created successfully.", }); @@ -78,8 +75,8 @@ export const SignInOptionalSetPasswordForm: React.FC = (props) => { state: "FAILED", first_time: false, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }); diff --git a/web/components/account/sign-in-forms/password.tsx b/web/components/account/sign-in-forms/password.tsx index 98719df63..7d51b0cf5 100644 --- a/web/components/account/sign-in-forms/password.tsx +++ b/web/components/account/sign-in-forms/password.tsx @@ -6,12 +6,11 @@ import { Eye, EyeOff, XCircle } from "lucide-react"; // services import { AuthService } from "services/auth.service"; // hooks -import useToast from "hooks/use-toast"; import { useApplication, useEventTracker } from "hooks/store"; // components import { ESignInSteps, ForgotPasswordPopover } from "components/account"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // types @@ -43,8 +42,6 @@ export const SignInPasswordForm: React.FC = observer((props) => { // states const [isSendingUniqueCode, setIsSendingUniqueCode] = useState(false); const [showPassword, setShowPassword] = useState(false); - // toast alert - const { setToastAlert } = useToast(); const { config: { envConfig }, } = useApplication(); @@ -83,8 +80,8 @@ export const SignInPasswordForm: React.FC = observer((props) => { await onSubmit(); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }) @@ -107,8 +104,8 @@ export const SignInPasswordForm: React.FC = observer((props) => { .generateUniqueCode({ email: emailFormValue }) .then(() => handleStepChange(ESignInSteps.USE_UNIQUE_CODE_FROM_PASSWORD)) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }) diff --git a/web/components/account/sign-in-forms/unique-code.tsx b/web/components/account/sign-in-forms/unique-code.tsx index 55dbe86e2..25ee4c462 100644 --- a/web/components/account/sign-in-forms/unique-code.tsx +++ b/web/components/account/sign-in-forms/unique-code.tsx @@ -5,11 +5,10 @@ import { XCircle } from "lucide-react"; import { AuthService } from "services/auth.service"; import { UserService } from "services/user.service"; // hooks -import useToast from "hooks/use-toast"; import useTimer from "hooks/use-timer"; import { useEventTracker } from "hooks/store"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // types @@ -42,8 +41,6 @@ export const SignInUniqueCodeForm: React.FC = (props) => { const { email, onSubmit, handleEmailClear, submitButtonText } = props; // states const [isRequestingNewCode, setIsRequestingNewCode] = useState(false); - // toast alert - const { setToastAlert } = useToast(); // store hooks const { captureEvent } = useEventTracker(); // timer @@ -84,8 +81,8 @@ export const SignInUniqueCodeForm: React.FC = (props) => { captureEvent(CODE_VERIFIED, { state: "FAILED", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }); @@ -101,8 +98,8 @@ export const SignInUniqueCodeForm: React.FC = (props) => { .generateUniqueCode(payload) .then(() => { setResendCodeTimer(30); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "A new unique code has been sent to your email.", }); @@ -113,8 +110,8 @@ export const SignInUniqueCodeForm: React.FC = (props) => { }); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }) diff --git a/web/components/account/sign-up-forms/email.tsx b/web/components/account/sign-up-forms/email.tsx index 0d5861b4e..b65ca95bf 100644 --- a/web/components/account/sign-up-forms/email.tsx +++ b/web/components/account/sign-up-forms/email.tsx @@ -4,10 +4,8 @@ import { XCircle } from "lucide-react"; import { observer } from "mobx-react-lite"; // services import { AuthService } from "services/auth.service"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // types @@ -27,7 +25,6 @@ const authService = new AuthService(); export const SignUpEmailForm: React.FC = observer((props) => { const { onSubmit, updateEmail } = props; // hooks - const { setToastAlert } = useToast(); const { control, formState: { errors, isSubmitting, isValid }, @@ -52,8 +49,8 @@ export const SignUpEmailForm: React.FC = observer((props) => { .emailCheck(payload) .then(() => onSubmit()) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }) diff --git a/web/components/account/sign-up-forms/optional-set-password.tsx b/web/components/account/sign-up-forms/optional-set-password.tsx index b49adabbb..651f2815f 100644 --- a/web/components/account/sign-up-forms/optional-set-password.tsx +++ b/web/components/account/sign-up-forms/optional-set-password.tsx @@ -3,14 +3,14 @@ import { Controller, useForm } from "react-hook-form"; // services import { AuthService } from "services/auth.service"; // hooks -import useToast from "hooks/use-toast"; import { useEventTracker } from "hooks/store"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; -// constants +// components import { ESignUpSteps } from "components/account"; +// constants import { PASSWORD_CREATE_SELECTED, PASSWORD_CREATE_SKIPPED, SETUP_PASSWORD } from "constants/event-tracker"; // icons import { Eye, EyeOff } from "lucide-react"; @@ -41,8 +41,6 @@ export const SignUpOptionalSetPasswordForm: React.FC = (props) => { const [showPassword, setShowPassword] = useState(false); // store hooks const { captureEvent } = useEventTracker(); - // toast alert - const { setToastAlert } = useToast(); // form info const { control, @@ -65,8 +63,8 @@ export const SignUpOptionalSetPasswordForm: React.FC = (props) => { await authService .setPassword(payload) .then(async () => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Password created successfully.", }); @@ -81,8 +79,8 @@ export const SignUpOptionalSetPasswordForm: React.FC = (props) => { state: "FAILED", first_time: true, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }); diff --git a/web/components/account/sign-up-forms/password.tsx b/web/components/account/sign-up-forms/password.tsx index 293e03ef8..5207a5024 100644 --- a/web/components/account/sign-up-forms/password.tsx +++ b/web/components/account/sign-up-forms/password.tsx @@ -5,10 +5,8 @@ import { Controller, useForm } from "react-hook-form"; import { Eye, EyeOff, XCircle } from "lucide-react"; // services import { AuthService } from "services/auth.service"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // types @@ -34,8 +32,6 @@ export const SignUpPasswordForm: React.FC = observer((props) => { const { onSubmit } = props; // states const [showPassword, setShowPassword] = useState(false); - // toast alert - const { setToastAlert } = useToast(); // form info const { control, @@ -59,8 +55,8 @@ export const SignUpPasswordForm: React.FC = observer((props) => { .passwordSignIn(payload) .then(async () => await onSubmit()) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }) diff --git a/web/components/account/sign-up-forms/unique-code.tsx b/web/components/account/sign-up-forms/unique-code.tsx index 1b54ef9eb..51705ea67 100644 --- a/web/components/account/sign-up-forms/unique-code.tsx +++ b/web/components/account/sign-up-forms/unique-code.tsx @@ -6,11 +6,10 @@ import { XCircle } from "lucide-react"; import { AuthService } from "services/auth.service"; import { UserService } from "services/user.service"; // hooks -import useToast from "hooks/use-toast"; import useTimer from "hooks/use-timer"; import { useEventTracker } from "hooks/store"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // types @@ -44,8 +43,6 @@ export const SignUpUniqueCodeForm: React.FC = (props) => { const [isRequestingNewCode, setIsRequestingNewCode] = useState(false); // store hooks const { captureEvent } = useEventTracker(); - // toast alert - const { setToastAlert } = useToast(); // timer const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(30); // form info @@ -84,8 +81,8 @@ export const SignUpUniqueCodeForm: React.FC = (props) => { captureEvent(CODE_VERIFIED, { state: "FAILED", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }); @@ -101,8 +98,8 @@ export const SignUpUniqueCodeForm: React.FC = (props) => { .generateUniqueCode(payload) .then(() => { setResendCodeTimer(30); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "A new unique code has been sent to your email.", }); @@ -112,8 +109,8 @@ export const SignUpUniqueCodeForm: React.FC = (props) => { }); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }) diff --git a/web/components/analytics/custom-analytics/sidebar/sidebar.tsx b/web/components/analytics/custom-analytics/sidebar/sidebar.tsx index bf1c80fea..aab7f874f 100644 --- a/web/components/analytics/custom-analytics/sidebar/sidebar.tsx +++ b/web/components/analytics/custom-analytics/sidebar/sidebar.tsx @@ -6,11 +6,10 @@ import { mutate } from "swr"; import { AnalyticsService } from "services/analytics.service"; // hooks import { useCycle, useModule, useProject, useUser, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { CustomAnalyticsSidebarHeader, CustomAnalyticsSidebarProjectsList } from "components/analytics"; // ui -import { Button, LayersIcon } from "@plane/ui"; +import { Button, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { CalendarDays, Download, RefreshCw } from "lucide-react"; // helpers @@ -34,8 +33,6 @@ export const CustomAnalyticsSidebar: React.FC = observer((props) => { // router const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId } = router.query; - // toast alert - const { setToastAlert } = useToast(); // store hooks const { currentUser } = useUser(); const { workspaceProjectIds, getProjectById } = useProject(); @@ -107,8 +104,8 @@ export const CustomAnalyticsSidebar: React.FC = observer((props) => { analyticsService .exportAnalytics(workspaceSlug.toString(), data) .then((res) => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: res.message, }); @@ -116,8 +113,8 @@ export const CustomAnalyticsSidebar: React.FC = observer((props) => { trackExportAnalytics(); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "There was some error in exporting the analytics. Please try again.", }) diff --git a/web/components/api-token/delete-token-modal.tsx b/web/components/api-token/delete-token-modal.tsx index 993289c10..472431df3 100644 --- a/web/components/api-token/delete-token-modal.tsx +++ b/web/components/api-token/delete-token-modal.tsx @@ -4,10 +4,8 @@ import { mutate } from "swr"; import { Dialog, Transition } from "@headlessui/react"; // services import { APITokenService } from "services/api_token.service"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IApiToken } from "@plane/types"; // fetch-keys @@ -25,8 +23,6 @@ export const DeleteApiTokenModal: FC = (props) => { const { isOpen, onClose, tokenId } = props; // states const [deleteLoading, setDeleteLoading] = useState(false); - // hooks - const { setToastAlert } = useToast(); // router const router = useRouter(); const { workspaceSlug } = router.query; @@ -44,8 +40,8 @@ export const DeleteApiTokenModal: FC = (props) => { apiTokenService .deleteApiToken(workspaceSlug.toString(), tokenId) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Token deleted successfully.", }); @@ -59,8 +55,8 @@ export const DeleteApiTokenModal: FC = (props) => { handleClose(); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error", message: err?.message ?? "Something went wrong. Please try again.", }) diff --git a/web/components/api-token/modal/create-token-modal.tsx b/web/components/api-token/modal/create-token-modal.tsx index b3fc3df78..c90e743bc 100644 --- a/web/components/api-token/modal/create-token-modal.tsx +++ b/web/components/api-token/modal/create-token-modal.tsx @@ -4,8 +4,8 @@ import { mutate } from "swr"; import { Dialog, Transition } from "@headlessui/react"; // services import { APITokenService } from "services/api_token.service"; -// hooks -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { CreateApiTokenForm, GeneratedTokenDetails } from "components/api-token"; // helpers @@ -32,8 +32,6 @@ export const CreateApiTokenModal: React.FC = (props) => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // toast alert - const { setToastAlert } = useToast(); const handleClose = () => { onClose(); @@ -76,10 +74,10 @@ export const CreateApiTokenModal: React.FC = (props) => { ); }) .catch((err) => { - setToastAlert({ - message: err.message, - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error", + message: err.message, }); throw err; diff --git a/web/components/api-token/modal/form.tsx b/web/components/api-token/modal/form.tsx index 77753e64d..9fc160815 100644 --- a/web/components/api-token/modal/form.tsx +++ b/web/components/api-token/modal/form.tsx @@ -3,10 +3,8 @@ import { add } from "date-fns"; import { Controller, useForm } from "react-hook-form"; import { DateDropdown } from "components/dropdowns"; import { Calendar } from "lucide-react"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button, CustomSelect, Input, TextArea, ToggleSwitch } from "@plane/ui"; +import { Button, CustomSelect, Input, TextArea, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { renderFormattedDate, renderFormattedPayloadDate } from "helpers/date-time.helper"; // types @@ -66,8 +64,6 @@ export const CreateApiTokenForm: React.FC = (props) => { const { handleClose, neverExpires, toggleNeverExpires, onSubmit } = props; // states const [customDate, setCustomDate] = useState(null); - // toast alert - const { setToastAlert } = useToast(); // form const { control, @@ -80,8 +76,8 @@ export const CreateApiTokenForm: React.FC = (props) => { const handleFormSubmit = async (data: IApiToken) => { // if never expires is toggled off, and the user has not selected a custom date or a predefined date, show an error if (!neverExpires && (!data.expired_at || (data.expired_at === "custom" && !customDate))) - return setToastAlert({ - type: "error", + return setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please select an expiration date.", }); diff --git a/web/components/api-token/modal/generated-token-details.tsx b/web/components/api-token/modal/generated-token-details.tsx index f28ea3481..fcae6b249 100644 --- a/web/components/api-token/modal/generated-token-details.tsx +++ b/web/components/api-token/modal/generated-token-details.tsx @@ -1,8 +1,6 @@ import { Copy } from "lucide-react"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button, Tooltip } from "@plane/ui"; +import { Button, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { renderFormattedDate } from "helpers/date-time.helper"; import { copyTextToClipboard } from "helpers/string.helper"; @@ -17,12 +15,10 @@ type Props = { export const GeneratedTokenDetails: React.FC = (props) => { const { handleClose, tokenDetails } = props; - const { setToastAlert } = useToast(); - const copyApiToken = (token: string) => { copyTextToClipboard(token).then(() => - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Token copied to clipboard.", }) diff --git a/web/components/command-palette/actions/issue-actions/actions-list.tsx b/web/components/command-palette/actions/issue-actions/actions-list.tsx index 55f72c85d..75bacb0c3 100644 --- a/web/components/command-palette/actions/issue-actions/actions-list.tsx +++ b/web/components/command-palette/actions/issue-actions/actions-list.tsx @@ -4,10 +4,8 @@ import { Command } from "cmdk"; import { LinkIcon, Signal, Trash2, UserMinus2, UserPlus2 } from "lucide-react"; // hooks import { useApplication, useUser, useIssues } from "hooks/store"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { DoubleCircleIcon, UserGroupIcon } from "@plane/ui"; +import { DoubleCircleIcon, UserGroupIcon, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; // types @@ -37,8 +35,6 @@ export const CommandPaletteIssueActions: React.FC = observer((props) => { } = useApplication(); const { currentUser } = useUser(); - const { setToastAlert } = useToast(); - const handleUpdateIssue = async (formData: Partial) => { if (!workspaceSlug || !projectId || !issueDetails) return; @@ -71,14 +67,14 @@ export const CommandPaletteIssueActions: React.FC = observer((props) => { const url = new URL(window.location.href); copyTextToClipboard(url.href) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Copied to clipboard", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Some error occurred", }); }); diff --git a/web/components/command-palette/actions/theme-actions.tsx b/web/components/command-palette/actions/theme-actions.tsx index 976a63c87..187bdfec8 100644 --- a/web/components/command-palette/actions/theme-actions.tsx +++ b/web/components/command-palette/actions/theme-actions.tsx @@ -5,7 +5,8 @@ import { Settings } from "lucide-react"; import { observer } from "mobx-react-lite"; // hooks import { useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // constants import { THEME_OPTIONS } from "constants/themes"; @@ -21,15 +22,14 @@ export const CommandPaletteThemeActions: FC = observer((props) => { const { updateCurrentUserTheme } = useUser(); // hooks const { setTheme } = useTheme(); - const { setToastAlert } = useToast(); const updateUserTheme = async (newTheme: string) => { setTheme(newTheme); return updateCurrentUserTheme(newTheme).catch(() => { - setToastAlert({ + setToast({ + type: TOAST_TYPE.ERROR, title: "Failed to save user theme settings!", - type: "error", }); }); }; diff --git a/web/components/command-palette/command-palette.tsx b/web/components/command-palette/command-palette.tsx index 396003589..e878489f4 100644 --- a/web/components/command-palette/command-palette.tsx +++ b/web/components/command-palette/command-palette.tsx @@ -4,7 +4,8 @@ import useSWR from "swr"; import { observer } from "mobx-react-lite"; // hooks import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { CommandModal, ShortcutsModal } from "components/command-palette"; import { BulkDeleteIssuesModal } from "components/core"; @@ -63,8 +64,6 @@ export const CommandPalette: FC = observer(() => { createIssueStoreType, } = commandPalette; - const { setToastAlert } = useToast(); - const { data: issueDetails } = useSWR( workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null, workspaceSlug && projectId && issueId @@ -78,18 +77,18 @@ export const CommandPalette: FC = observer(() => { const url = new URL(window.location.href); copyTextToClipboard(url.href) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Copied to clipboard", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Some error occurred", }); }); - }, [setToastAlert, issueId]); + }, [issueId]); const handleKeyDown = useCallback( (e: KeyboardEvent) => { diff --git a/web/components/core/modals/bulk-delete-issues-modal.tsx b/web/components/core/modals/bulk-delete-issues-modal.tsx index 39be2872b..7eeb79174 100644 --- a/web/components/core/modals/bulk-delete-issues-modal.tsx +++ b/web/components/core/modals/bulk-delete-issues-modal.tsx @@ -6,10 +6,8 @@ import { SubmitHandler, useForm } from "react-hook-form"; import { Combobox, Dialog, Transition } from "@headlessui/react"; // services import { IssueService } from "services/issue"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button, LayersIcon } from "@plane/ui"; +import { Button, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { Search } from "lucide-react"; // types @@ -55,8 +53,6 @@ export const BulkDeleteIssuesModal: React.FC = observer((props) => { : null ); - const { setToastAlert } = useToast(); - const { handleSubmit, watch, @@ -79,8 +75,8 @@ export const BulkDeleteIssuesModal: React.FC = observer((props) => { if (!workspaceSlug || !projectId) return; if (!data.delete_issue_ids || data.delete_issue_ids.length === 0) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please select at least one issue.", }); @@ -91,16 +87,16 @@ export const BulkDeleteIssuesModal: React.FC = observer((props) => { await removeBulkIssues(workspaceSlug as string, projectId as string, data.delete_issue_ids) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Issues deleted successfully!", }); handleClose(); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong. Please try again.", }) diff --git a/web/components/core/modals/existing-issues-list-modal.tsx b/web/components/core/modals/existing-issues-list-modal.tsx index 1b6a1e76b..a1f8bfaa4 100644 --- a/web/components/core/modals/existing-issues-list-modal.tsx +++ b/web/components/core/modals/existing-issues-list-modal.tsx @@ -3,11 +3,9 @@ import { Combobox, Dialog, Transition } from "@headlessui/react"; import { Rocket, Search, X } from "lucide-react"; // services import { ProjectService } from "services/project"; -// hooks -import useToast from "hooks/use-toast"; import useDebounce from "hooks/use-debounce"; // ui -import { Button, LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui"; +import { Button, LayersIcon, Loader, ToggleSwitch, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // types import { ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types"; @@ -43,8 +41,6 @@ export const ExistingIssuesListModal: React.FC = (props) => { const debouncedSearchTerm: string = useDebounce(searchTerm, 500); - const { setToastAlert } = useToast(); - const handleClose = () => { onClose(); setSearchTerm(""); @@ -54,8 +50,8 @@ export const ExistingIssuesListModal: React.FC = (props) => { const onSubmit = async () => { if (selectedIssues.length === 0) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please select at least one issue.", }); @@ -69,9 +65,9 @@ export const ExistingIssuesListModal: React.FC = (props) => { handleClose(); - setToastAlert({ + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success", - type: "success", message: `Issue${selectedIssues.length > 1 ? "s" : ""} added successfully`, }); }; diff --git a/web/components/core/modals/gpt-assistant-popover.tsx b/web/components/core/modals/gpt-assistant-popover.tsx index 590015e12..49c2f4326 100644 --- a/web/components/core/modals/gpt-assistant-popover.tsx +++ b/web/components/core/modals/gpt-assistant-popover.tsx @@ -3,10 +3,9 @@ import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; // services import { AIService } from "services/ai.service"; // hooks -import useToast from "hooks/use-toast"; import { usePopper } from "react-popper"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // components import { RichReadOnlyEditorWithRef } from "@plane/rich-text-editor"; import { Popover, Transition } from "@headlessui/react"; @@ -44,8 +43,6 @@ export const GptAssistantPopover: React.FC = (props) => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // toast alert - const { setToastAlert } = useToast(); // popper const { styles, attributes } = usePopper(referenceElement, popperElement, { placement: placement ?? "auto", @@ -78,8 +75,8 @@ export const GptAssistantPopover: React.FC = (props) => { ? error || "You have reached the maximum number of requests of 50 requests per month per user." : error || "Some error occurred. Please try again."; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: errorMessage, }); @@ -104,8 +101,8 @@ export const GptAssistantPopover: React.FC = (props) => { }; const handleInvalidTask = () => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please enter some task to get AI assistance.", }); diff --git a/web/components/core/modals/user-image-upload-modal.tsx b/web/components/core/modals/user-image-upload-modal.tsx index 6debc2c15..3d0fbb9ee 100644 --- a/web/components/core/modals/user-image-upload-modal.tsx +++ b/web/components/core/modals/user-image-upload-modal.tsx @@ -6,10 +6,8 @@ import { Transition, Dialog } from "@headlessui/react"; import { useApplication } from "hooks/store"; // services import { FileService } from "services/file.service"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { UserCircle2 } from "lucide-react"; // constants @@ -32,8 +30,6 @@ export const UserImageUploadModal: React.FC = observer((props) => { // states const [image, setImage] = useState(null); const [isImageUploading, setIsImageUploading] = useState(false); - // toast alert - const { setToastAlert } = useToast(); // store hooks const { config: { envConfig }, @@ -76,8 +72,8 @@ export const UserImageUploadModal: React.FC = observer((props) => { if (value) fileService.deleteUserFile(value); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }) diff --git a/web/components/core/modals/workspace-image-upload-modal.tsx b/web/components/core/modals/workspace-image-upload-modal.tsx index e04ccf820..eec62b919 100644 --- a/web/components/core/modals/workspace-image-upload-modal.tsx +++ b/web/components/core/modals/workspace-image-upload-modal.tsx @@ -7,10 +7,8 @@ import { Transition, Dialog } from "@headlessui/react"; import { useApplication, useWorkspace } from "hooks/store"; // services import { FileService } from "services/file.service"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { UserCircle2 } from "lucide-react"; // constants @@ -37,8 +35,6 @@ export const WorkspaceImageUploadModal: React.FC = observer((props) => { const router = useRouter(); const { workspaceSlug } = router.query; - const { setToastAlert } = useToast(); - const { config: { envConfig }, } = useApplication(); @@ -83,8 +79,8 @@ export const WorkspaceImageUploadModal: React.FC = observer((props) => { if (value && currentWorkspace) fileService.deleteFile(currentWorkspace.id, value); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }) diff --git a/web/components/core/sidebar/links-list.tsx b/web/components/core/sidebar/links-list.tsx index 48a5e16b7..6b987e308 100644 --- a/web/components/core/sidebar/links-list.tsx +++ b/web/components/core/sidebar/links-list.tsx @@ -1,5 +1,5 @@ // ui -import { ExternalLinkIcon, Tooltip } from "@plane/ui"; +import { ExternalLinkIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { Pencil, Trash2, LinkIcon } from "lucide-react"; // helpers @@ -7,7 +7,6 @@ import { calculateTimeAgo } from "helpers/date-time.helper"; // types import { ILinkDetails, UserAuth } from "@plane/types"; // hooks -import useToast from "hooks/use-toast"; import { observer } from "mobx-react"; import { useMeasure } from "@nivo/core"; import { useMember } from "hooks/store"; @@ -20,18 +19,16 @@ type Props = { }; export const LinksList: React.FC = observer(({ links, handleDeleteLink, handleEditLink, userAuth }) => { - // toast - const { setToastAlert } = useToast(); const { getUserDetails } = useMember(); const isNotAllowed = userAuth.isGuest || userAuth.isViewer; const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text); - setToastAlert({ - message: "The URL has been successfully copied to your clipboard", - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Copied to clipboard", + message: "The URL has been successfully copied to your clipboard", }); }; diff --git a/web/components/cycles/active-cycle-details.tsx b/web/components/cycles/active-cycle-details.tsx index 425ce7df3..bc22cb8ab 100644 --- a/web/components/cycles/active-cycle-details.tsx +++ b/web/components/cycles/active-cycle-details.tsx @@ -5,7 +5,6 @@ import useSWR from "swr"; import { useTheme } from "next-themes"; // hooks import { useCycle, useIssues, useMember, useProject, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui import { SingleProgressStats } from "components/core"; import { @@ -18,6 +17,7 @@ import { PriorityIcon, Avatar, CycleGroupIcon, + setPromiseToast, } from "@plane/ui"; // components import ProgressChart from "components/core/sidebar/progress-chart"; @@ -60,8 +60,6 @@ export const ActiveCycleDetails: React.FC = observer((props } = useCycle(); const { currentProjectDetails } = useProject(); const { getUserDetails } = useMember(); - // toast alert - const { setToastAlert } = useToast(); const { isLoading } = useSWR( workspaceSlug && projectId ? `PROJECT_ACTIVE_CYCLE_${projectId}` : null, @@ -119,12 +117,18 @@ export const ActiveCycleDetails: React.FC = observer((props e.preventDefault(); if (!workspaceSlug || !projectId) return; - addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), activeCycle.id).catch(() => { - setToastAlert({ - type: "error", + const addToFavoritePromise = addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), activeCycle.id); + + setPromiseToast(addToFavoritePromise, { + loading: "Adding cycle to favorites...", + success: { + title: "Success!", + message: () => "Cycle added to favorites.", + }, + error: { title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", - }); + message: () => "Couldn't add the cycle to favorites. Please try again.", + }, }); }; @@ -132,12 +136,22 @@ export const ActiveCycleDetails: React.FC = observer((props e.preventDefault(); if (!workspaceSlug || !projectId) return; - removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), activeCycle.id).catch(() => { - setToastAlert({ - type: "error", + const removeFromFavoritePromise = removeCycleFromFavorites( + workspaceSlug?.toString(), + projectId.toString(), + activeCycle.id + ); + + setPromiseToast(removeFromFavoritePromise, { + loading: "Removing cycle from favorites...", + success: { + title: "Success!", + message: () => "Cycle removed from favorites.", + }, + error: { title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", - }); + message: () => "Couldn't remove the cycle from favorites. Please try again.", + }, }); }; diff --git a/web/components/cycles/cycles-board-card.tsx b/web/components/cycles/cycles-board-card.tsx index 7d6b1e000..72af8409d 100644 --- a/web/components/cycles/cycles-board-card.tsx +++ b/web/components/cycles/cycles-board-card.tsx @@ -4,11 +4,20 @@ import Link from "next/link"; import { observer } from "mobx-react"; // hooks import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles"; // ui -import { Avatar, AvatarGroup, CustomMenu, Tooltip, LayersIcon, CycleGroupIcon } from "@plane/ui"; +import { + Avatar, + AvatarGroup, + CustomMenu, + Tooltip, + LayersIcon, + CycleGroupIcon, + TOAST_TYPE, + setToast, + setPromiseToast, +} from "@plane/ui"; // icons import { Info, LinkIcon, Pencil, Star, Trash2 } from "lucide-react"; // helpers @@ -41,8 +50,6 @@ export const CyclesBoardCard: FC = observer((props) => { } = useUser(); const { addCycleToFavorites, removeCycleFromFavorites, getCycleById } = useCycle(); const { getUserDetails } = useMember(); - // toast alert - const { setToastAlert } = useToast(); // computed const cycleDetails = getCycleById(cycleId); @@ -81,8 +88,8 @@ export const CyclesBoardCard: FC = observer((props) => { const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Cycle link copied to clipboard.", }); @@ -93,42 +100,56 @@ export const CyclesBoardCard: FC = observer((props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId) - .then(() => { + const addToFavoritePromise = addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).then( + () => { captureEvent(CYCLE_FAVORITED, { cycle_id: cycleId, element: "Grid layout", state: "SUCCESS", }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", - }); - }); + } + ); + + setPromiseToast(addToFavoritePromise, { + loading: "Adding cycle to favorites...", + success: { + title: "Success!", + message: () => "Cycle added to favorites.", + }, + error: { + title: "Error!", + message: () => "Couldn't add the cycle to favorites. Please try again.", + }, + }); }; const handleRemoveFromFavorites = (e: MouseEvent) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId) - .then(() => { - captureEvent(CYCLE_UNFAVORITED, { - cycle_id: cycleId, - element: "Grid layout", - state: "SUCCESS", - }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", - }); + const removeFromFavoritePromise = removeCycleFromFavorites( + workspaceSlug?.toString(), + projectId.toString(), + cycleId + ).then(() => { + captureEvent(CYCLE_UNFAVORITED, { + cycle_id: cycleId, + element: "Grid layout", + state: "SUCCESS", }); + }); + + setPromiseToast(removeFromFavoritePromise, { + loading: "Removing cycle from favorites...", + success: { + title: "Success!", + message: () => "Cycle removed from favorites.", + }, + error: { + title: "Error!", + message: () => "Couldn't remove the cycle from favorites. Please try again.", + }, + }); }; const handleEditCycle = (e: MouseEvent) => { diff --git a/web/components/cycles/cycles-list-item.tsx b/web/components/cycles/cycles-list-item.tsx index 31958cd84..9ab2e3de8 100644 --- a/web/components/cycles/cycles-list-item.tsx +++ b/web/components/cycles/cycles-list-item.tsx @@ -4,11 +4,20 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react"; // hooks import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles"; // ui -import { CustomMenu, Tooltip, CircularProgressIndicator, CycleGroupIcon, AvatarGroup, Avatar } from "@plane/ui"; +import { + CustomMenu, + Tooltip, + CircularProgressIndicator, + CycleGroupIcon, + AvatarGroup, + Avatar, + TOAST_TYPE, + setToast, + setPromiseToast, +} from "@plane/ui"; // icons import { Check, Info, LinkIcon, Pencil, Star, Trash2, User2 } from "lucide-react"; // helpers @@ -45,8 +54,6 @@ export const CyclesListItem: FC = observer((props) => { } = useUser(); const { getCycleById, addCycleToFavorites, removeCycleFromFavorites } = useCycle(); const { getUserDetails } = useMember(); - // toast alert - const { setToastAlert } = useToast(); const handleCopyText = (e: MouseEvent) => { e.preventDefault(); @@ -54,8 +61,8 @@ export const CyclesListItem: FC = observer((props) => { const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Cycle link copied to clipboard.", }); @@ -66,42 +73,56 @@ export const CyclesListItem: FC = observer((props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId) - .then(() => { + const addToFavoritePromise = addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).then( + () => { captureEvent(CYCLE_FAVORITED, { cycle_id: cycleId, element: "List layout", state: "SUCCESS", }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", - }); - }); + } + ); + + setPromiseToast(addToFavoritePromise, { + loading: "Adding cycle to favorites...", + success: { + title: "Success!", + message: () => "Cycle added to favorites.", + }, + error: { + title: "Error!", + message: () => "Couldn't add the cycle to favorites. Please try again.", + }, + }); }; const handleRemoveFromFavorites = (e: MouseEvent) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId) - .then(() => { - captureEvent(CYCLE_UNFAVORITED, { - cycle_id: cycleId, - element: "List layout", - state: "SUCCESS", - }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", - }); + const removeFromFavoritePromise = removeCycleFromFavorites( + workspaceSlug?.toString(), + projectId.toString(), + cycleId + ).then(() => { + captureEvent(CYCLE_UNFAVORITED, { + cycle_id: cycleId, + element: "List layout", + state: "SUCCESS", }); + }); + + setPromiseToast(removeFromFavoritePromise, { + loading: "Removing cycle from favorites...", + success: { + title: "Success!", + message: () => "Cycle removed from favorites.", + }, + error: { + title: "Error!", + message: () => "Couldn't remove the cycle from favorites. Please try again.", + }, + }); }; const handleEditCycle = (e: MouseEvent) => { diff --git a/web/components/cycles/delete-modal.tsx b/web/components/cycles/delete-modal.tsx index 5dc0306ab..239fe6a66 100644 --- a/web/components/cycles/delete-modal.tsx +++ b/web/components/cycles/delete-modal.tsx @@ -5,9 +5,8 @@ import { observer } from "mobx-react-lite"; import { AlertTriangle } from "lucide-react"; // hooks import { useEventTracker, useCycle } from "hooks/store"; -import useToast from "hooks/use-toast"; // components -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import { ICycle } from "@plane/types"; // constants @@ -31,8 +30,6 @@ export const CycleDeleteModal: React.FC = observer((props) => { // store hooks const { captureCycleEvent } = useEventTracker(); const { deleteCycle } = useCycle(); - // toast alert - const { setToastAlert } = useToast(); const formSubmit = async () => { if (!cycle) return; @@ -41,8 +38,8 @@ export const CycleDeleteModal: React.FC = observer((props) => { try { await deleteCycle(workspaceSlug, projectId, cycle.id) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Cycle deleted successfully.", }); @@ -62,8 +59,8 @@ export const CycleDeleteModal: React.FC = observer((props) => { handleClose(); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Warning!", message: "Something went wrong please try again later.", }); diff --git a/web/components/cycles/modal.tsx b/web/components/cycles/modal.tsx index b22afb2b4..1d60f1dc4 100644 --- a/web/components/cycles/modal.tsx +++ b/web/components/cycles/modal.tsx @@ -4,10 +4,11 @@ import { Dialog, Transition } from "@headlessui/react"; import { CycleService } from "services/cycle.service"; // hooks import { useEventTracker, useCycle, useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; import useLocalStorage from "hooks/use-local-storage"; // components import { CycleForm } from "components/cycles"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // types import type { CycleDateCheckData, ICycle, TCycleView } from "@plane/types"; // constants @@ -32,8 +33,6 @@ export const CycleCreateUpdateModal: React.FC = (props) => { const { captureCycleEvent } = useEventTracker(); const { workspaceProjectIds } = useProject(); const { createCycle, updateCycleDetails } = useCycle(); - // toast alert - const { setToastAlert } = useToast(); const { setValue: setCycleTab } = useLocalStorage("cycle_tab", "active"); @@ -43,8 +42,8 @@ export const CycleCreateUpdateModal: React.FC = (props) => { const selectedProjectId = payload.project_id ?? projectId.toString(); await createCycle(workspaceSlug, selectedProjectId, payload) .then((res) => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Cycle created successfully.", }); @@ -54,8 +53,8 @@ export const CycleCreateUpdateModal: React.FC = (props) => { }); }) .catch((err) => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err.detail ?? "Error in creating cycle. Please try again.", }); @@ -77,8 +76,8 @@ export const CycleCreateUpdateModal: React.FC = (props) => { eventName: CYCLE_UPDATED, payload: { ...res, changed_properties: changed_properties, state: "SUCCESS" }, }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Cycle updated successfully.", }); @@ -88,8 +87,8 @@ export const CycleCreateUpdateModal: React.FC = (props) => { eventName: CYCLE_UPDATED, payload: { ...payload, state: "FAILED" }, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err.detail ?? "Error in updating cycle. Please try again.", }); @@ -138,8 +137,8 @@ export const CycleCreateUpdateModal: React.FC = (props) => { } handleClose(); } else - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "You already have a cycle on the given dates, if you want to create a draft cycle, remove the dates.", }); diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index 646736bd2..f01c840f1 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -8,13 +8,12 @@ import isEmpty from "lodash/isEmpty"; import { CycleService } from "services/cycle.service"; // hooks import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { SidebarProgressStats } from "components/core"; import ProgressChart from "components/core/sidebar/progress-chart"; import { CycleDeleteModal } from "components/cycles/delete-modal"; // ui -import { Avatar, CustomMenu, Loader, LayersIcon } from "@plane/ui"; +import { Avatar, CustomMenu, Loader, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { ChevronDown, LinkIcon, Trash2, UserCircle2, AlertCircle, ChevronRight, CalendarClock } from "lucide-react"; // helpers @@ -60,8 +59,6 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { // derived values const cycleDetails = getCycleById(cycleId); const cycleOwnerDetails = cycleDetails ? getUserDetails(cycleDetails.owned_by_id) : undefined; - // toast alert - const { setToastAlert } = useToast(); // form info const { control, reset } = useForm({ defaultValues, @@ -98,15 +95,15 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { const handleCopyText = () => { copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Cycle link copied to clipboard.", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Some error occurred", }); }); @@ -147,14 +144,14 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { if (isDateValid) { submitChanges(payload, "date_range"); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Cycle updated successfully.", }); } else { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "You already have a cycle on the given dates, if you want to create a draft cycle, you can do that by removing both the dates.", diff --git a/web/components/cycles/transfer-issues-modal.tsx b/web/components/cycles/transfer-issues-modal.tsx index adff19545..be3b26a7b 100644 --- a/web/components/cycles/transfer-issues-modal.tsx +++ b/web/components/cycles/transfer-issues-modal.tsx @@ -3,10 +3,10 @@ import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; // hooks -import useToast from "hooks/use-toast"; import { useCycle, useIssues } from "hooks/store"; +// ui +import { ContrastIcon, TransferIcon, TOAST_TYPE, setToast } from "@plane/ui"; //icons -import { ContrastIcon, TransferIcon } from "@plane/ui"; import { AlertCircle, Search, X } from "lucide-react"; // constants import { EIssuesStoreType } from "constants/issue"; @@ -30,23 +30,21 @@ export const TransferIssuesModal: React.FC = observer((props) => { const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; - const { setToastAlert } = useToast(); - const transferIssue = async (payload: any) => { if (!workspaceSlug || !projectId || !cycleId) return; // TODO: import transferIssuesFromCycle from store await transferIssuesFromCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), payload) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Issues transferred successfully", message: "Issues have been transferred successfully", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Issues cannot be transfer. Please try again.", }); diff --git a/web/components/estimates/create-update-estimate-modal.tsx b/web/components/estimates/create-update-estimate-modal.tsx index 0a607e88d..1ca39c84a 100644 --- a/web/components/estimates/create-update-estimate-modal.tsx +++ b/web/components/estimates/create-update-estimate-modal.tsx @@ -5,9 +5,8 @@ import { Dialog, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; // store hooks import { useEstimate } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, Input, TextArea } from "@plane/ui"; +import { Button, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { checkDuplicates } from "helpers/array.helper"; // types @@ -40,8 +39,6 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { // store hooks const { createEstimate, updateEstimate } = useEstimate(); // form info - // toast alert - const { setToastAlert } = useToast(); const { formState: { errors, isSubmitting }, handleSubmit, @@ -67,8 +64,8 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { const error = err?.error; const errorString = Array.isArray(error) ? error[0] : error; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: errorString ?? err.status === 400 @@ -89,8 +86,8 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { const error = err?.error; const errorString = Array.isArray(error) ? error[0] : error; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: errorString ?? "Estimate could not be updated. Please try again.", }); @@ -99,8 +96,8 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { const onSubmit = async (formData: FormValues) => { if (!formData.name || formData.name === "") { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Estimate title cannot be empty.", }); @@ -115,8 +112,8 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { formData.value5 === "" || formData.value6 === "" ) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Estimate point cannot be empty.", }); @@ -131,8 +128,8 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { formData.value5.length > 20 || formData.value6.length > 20 ) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Estimate point cannot have more than 20 characters.", }); @@ -149,8 +146,8 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { formData.value6, ]) ) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Estimate points cannot have duplicate values.", }); diff --git a/web/components/estimates/delete-estimate-modal.tsx b/web/components/estimates/delete-estimate-modal.tsx index 8055ddb90..ac51d2312 100644 --- a/web/components/estimates/delete-estimate-modal.tsx +++ b/web/components/estimates/delete-estimate-modal.tsx @@ -5,11 +5,10 @@ import { observer } from "mobx-react-lite"; import { AlertTriangle } from "lucide-react"; // store hooks import { useEstimate } from "hooks/store"; -import useToast from "hooks/use-toast"; // types import { IEstimate } from "@plane/types"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; type Props = { isOpen: boolean; @@ -26,8 +25,6 @@ export const DeleteEstimateModal: React.FC = observer((props) => { const { workspaceSlug, projectId } = router.query; // store hooks const { deleteEstimate } = useEstimate(); - // toast alert - const { setToastAlert } = useToast(); const handleEstimateDelete = () => { if (!workspaceSlug || !projectId) return; @@ -43,8 +40,8 @@ export const DeleteEstimateModal: React.FC = observer((props) => { const error = err?.error; const errorString = Array.isArray(error) ? error[0] : error; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: errorString ?? "Estimate could not be deleted. Please try again", }); diff --git a/web/components/estimates/estimate-list-item.tsx b/web/components/estimates/estimate-list-item.tsx index b6effa711..37932a0ac 100644 --- a/web/components/estimates/estimate-list-item.tsx +++ b/web/components/estimates/estimate-list-item.tsx @@ -3,9 +3,8 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // hooks import { useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, CustomMenu } from "@plane/ui"; +import { Button, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; //icons import { Pencil, Trash2 } from "lucide-react"; // helpers @@ -26,8 +25,6 @@ export const EstimateListItem: React.FC = observer((props) => { const { workspaceSlug, projectId } = router.query; // store hooks const { currentProjectDetails, updateProject } = useProject(); - // hooks - const { setToastAlert } = useToast(); const handleUseEstimate = async () => { if (!workspaceSlug || !projectId) return; @@ -38,8 +35,8 @@ export const EstimateListItem: React.FC = observer((props) => { const error = err?.error; const errorString = Array.isArray(error) ? error[0] : error; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: errorString ?? "Estimate points could not be used. Please try again.", }); diff --git a/web/components/estimates/estimates-list.tsx b/web/components/estimates/estimates-list.tsx index 1dabc6181..711f713a6 100644 --- a/web/components/estimates/estimates-list.tsx +++ b/web/components/estimates/estimates-list.tsx @@ -4,12 +4,11 @@ import { observer } from "mobx-react-lite"; import { useTheme } from "next-themes"; // store hooks import { useEstimate, useProject, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "components/estimates"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui -import { Button, Loader } from "@plane/ui"; +import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IEstimate } from "@plane/types"; // helpers @@ -31,8 +30,6 @@ export const EstimatesList: React.FC = observer(() => { const { updateProject, currentProjectDetails } = useProject(); const { projectEstimates, getProjectEstimateById } = useEstimate(); const { currentUser } = useUser(); - // toast alert - const { setToastAlert } = useToast(); const editEstimate = (estimate: IEstimate) => { setEstimateFormOpen(true); @@ -50,8 +47,8 @@ export const EstimatesList: React.FC = observer(() => { const error = err?.error; const errorString = Array.isArray(error) ? error[0] : error; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: errorString ?? "Estimate could not be disabled. Please try again", }); diff --git a/web/components/exporter/export-modal.tsx b/web/components/exporter/export-modal.tsx index b1f529775..f38550b3a 100644 --- a/web/components/exporter/export-modal.tsx +++ b/web/components/exporter/export-modal.tsx @@ -6,10 +6,8 @@ import { Dialog, Transition } from "@headlessui/react"; import { useProject } from "hooks/store"; // services import { ProjectExportService } from "services/project"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button, CustomSearchSelect } from "@plane/ui"; +import { Button, CustomSearchSelect, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IUser, IImporterService } from "@plane/types"; @@ -34,8 +32,6 @@ export const Exporter: React.FC = observer((props) => { const { workspaceSlug } = router.query; // store hooks const { workspaceProjectIds, getProjectById } = useProject(); - // toast alert - const { setToastAlert } = useToast(); const options = workspaceProjectIds?.map((projectId) => { const projectDetails = getProjectById(projectId); @@ -71,8 +67,8 @@ export const Exporter: React.FC = observer((props) => { mutateServices(); router.push(`/${workspaceSlug}/settings/exports`); setExportLoading(false); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Export Successful", message: `You will be able to download the exported ${ provider === "csv" ? "CSV" : provider === "xlsx" ? "Excel" : provider === "json" ? "JSON" : "" @@ -81,8 +77,8 @@ export const Exporter: React.FC = observer((props) => { }) .catch(() => { setExportLoading(false); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Export was unsuccessful. Please try again.", }); diff --git a/web/components/inbox/inbox-issue-actions.tsx b/web/components/inbox/inbox-issue-actions.tsx index 48d9157c6..200d541ab 100644 --- a/web/components/inbox/inbox-issue-actions.tsx +++ b/web/components/inbox/inbox-issue-actions.tsx @@ -5,7 +5,6 @@ import { DayPicker } from "react-day-picker"; import { Popover } from "@headlessui/react"; // hooks import { useUser, useInboxIssues, useIssueDetail, useWorkspace, useEventTracker } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { AcceptIssueModal, @@ -14,7 +13,7 @@ import { SelectDuplicateInboxIssueModal, } from "components/inbox"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Trash2, XCircle } from "lucide-react"; // types @@ -51,7 +50,6 @@ export const InboxIssueActionsHeader: FC = observer((p currentUser, membership: { currentProjectRole }, } = useUser(); - const { setToastAlert } = useToast(); // states const [date, setDate] = useState(new Date()); @@ -74,8 +72,8 @@ export const InboxIssueActionsHeader: FC = observer((p if (!workspaceSlug || !projectId || !inboxId || !inboxIssueId) throw new Error("Missing required parameters"); await updateInboxIssueStatus(workspaceSlug, projectId, inboxId, inboxIssueId, data); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong while updating inbox status. Please try again.", }); @@ -98,8 +96,8 @@ export const InboxIssueActionsHeader: FC = observer((p pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`, }); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong while deleting inbox issue. Please try again.", }); @@ -122,7 +120,6 @@ export const InboxIssueActionsHeader: FC = observer((p inboxIssueId, updateInboxIssueStatus, removeInboxIssue, - setToastAlert, captureIssueEvent, router, ] diff --git a/web/components/inbox/modals/create-issue-modal.tsx b/web/components/inbox/modals/create-issue-modal.tsx index 84c4bef1e..2ef65c497 100644 --- a/web/components/inbox/modals/create-issue-modal.tsx +++ b/web/components/inbox/modals/create-issue-modal.tsx @@ -7,7 +7,6 @@ import { RichTextEditorWithRef } from "@plane/rich-text-editor"; import { Sparkle } from "lucide-react"; // hooks import { useApplication, useEventTracker, useWorkspace, useInboxIssues, useMention } from "hooks/store"; -import useToast from "hooks/use-toast"; // services import { FileService } from "services/file.service"; import { AIService } from "services/ai.service"; @@ -15,7 +14,7 @@ import { AIService } from "services/ai.service"; import { PriorityDropdown } from "components/dropdowns"; import { GptAssistantPopover } from "components/core"; // ui -import { Button, Input, ToggleSwitch } from "@plane/ui"; +import { Button, Input, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; // types import { TIssue } from "@plane/types"; // constants @@ -46,9 +45,6 @@ export const CreateInboxIssueModal: React.FC = observer((props) => { const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); // refs const editorRef = useRef(null); - // toast alert - const { setToastAlert } = useToast(); - const { mentionHighlights, mentionSuggestions } = useMention(); // router const router = useRouter(); const { workspaceSlug, projectId, inboxId } = router.query as { @@ -56,6 +52,8 @@ export const CreateInboxIssueModal: React.FC = observer((props) => { projectId: string; inboxId: string; }; + // hooks + const { mentionHighlights, mentionSuggestions } = useMention(); const workspaceStore = useWorkspace(); const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug as string)?.id as string; @@ -138,8 +136,8 @@ export const CreateInboxIssueModal: React.FC = observer((props) => { }) .then((res) => { if (res.response === "") - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Issue title isn't informative enough to generate the description. Please try with a different title.", @@ -150,14 +148,14 @@ export const CreateInboxIssueModal: React.FC = observer((props) => { const error = err?.data?.error; if (err.status === 429) - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error || "You have reached the maximum number of requests of 50 requests per month per user.", }); else - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error || "Some error occurred. Please try again.", }); diff --git a/web/components/inbox/modals/select-duplicate.tsx b/web/components/inbox/modals/select-duplicate.tsx index e4acca626..c9e0a1dad 100644 --- a/web/components/inbox/modals/select-duplicate.tsx +++ b/web/components/inbox/modals/select-duplicate.tsx @@ -2,13 +2,10 @@ import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; import { Combobox, Dialog, Transition } from "@headlessui/react"; - -// hooks -import useToast from "hooks/use-toast"; // services import { IssueService } from "services/issue"; // ui -import { Button, LayersIcon } from "@plane/ui"; +import { Button, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { Search } from "lucide-react"; // fetch-keys @@ -30,8 +27,6 @@ export const SelectDuplicateInboxIssueModal: React.FC = (props) => { const [query, setQuery] = useState(""); const [selectedItem, setSelectedItem] = useState(""); - const { setToastAlert } = useToast(); - const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; @@ -62,9 +57,9 @@ export const SelectDuplicateInboxIssueModal: React.FC = (props) => { const handleSubmit = () => { if (!selectedItem || selectedItem.length === 0) - return setToastAlert({ + return setToast({ title: "Error", - type: "error", + type: TOAST_TYPE.ERROR, }); onSubmit(selectedItem); handleClose(); diff --git a/web/components/instance/ai-form.tsx b/web/components/instance/ai-form.tsx index 50ea90096..250feb511 100644 --- a/web/components/instance/ai-form.tsx +++ b/web/components/instance/ai-form.tsx @@ -2,12 +2,11 @@ import { FC, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { Eye, EyeOff } from "lucide-react"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IFormattedInstanceConfiguration } from "@plane/types"; // hooks import { useApplication } from "hooks/store"; -import useToast from "hooks/use-toast"; export interface IInstanceAIForm { config: IFormattedInstanceConfiguration; @@ -24,8 +23,6 @@ export const InstanceAIForm: FC = (props) => { const [showPassword, setShowPassword] = useState(false); // store const { instance: instanceStore } = useApplication(); - // toast - const { setToastAlert } = useToast(); // form data const { handleSubmit, @@ -44,9 +41,9 @@ export const InstanceAIForm: FC = (props) => { await instanceStore .updateInstanceConfigurations(payload) .then(() => - setToastAlert({ + setToast({ title: "Success", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "AI Settings updated successfully", }) ) diff --git a/web/components/instance/email-form.tsx b/web/components/instance/email-form.tsx index 9a97f3288..9e9e4a865 100644 --- a/web/components/instance/email-form.tsx +++ b/web/components/instance/email-form.tsx @@ -1,13 +1,12 @@ import { FC, useState } from "react"; import { Controller, useForm } from "react-hook-form"; // ui -import { Button, Input, ToggleSwitch } from "@plane/ui"; +import { Button, Input, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; import { Eye, EyeOff } from "lucide-react"; // types import { IFormattedInstanceConfiguration } from "@plane/types"; // hooks import { useApplication } from "hooks/store"; -import useToast from "hooks/use-toast"; export interface IInstanceEmailForm { config: IFormattedInstanceConfiguration; @@ -29,8 +28,6 @@ export const InstanceEmailForm: FC = (props) => { const [showPassword, setShowPassword] = useState(false); // store hooks const { instance: instanceStore } = useApplication(); - // toast - const { setToastAlert } = useToast(); // form data const { handleSubmit, @@ -55,9 +52,9 @@ export const InstanceEmailForm: FC = (props) => { await instanceStore .updateInstanceConfigurations(payload) .then(() => - setToastAlert({ + setToast({ title: "Success", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Email Settings updated successfully", }) ) diff --git a/web/components/instance/general-form.tsx b/web/components/instance/general-form.tsx index 7fa06265f..6fedc8831 100644 --- a/web/components/instance/general-form.tsx +++ b/web/components/instance/general-form.tsx @@ -1,12 +1,11 @@ import { FC } from "react"; import { Controller, useForm } from "react-hook-form"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IInstance, IInstanceAdmin } from "@plane/types"; // hooks import { useApplication } from "hooks/store"; -import useToast from "hooks/use-toast"; export interface IInstanceGeneralForm { instance: IInstance; @@ -22,8 +21,6 @@ export const InstanceGeneralForm: FC = (props) => { const { instance, instanceAdmins } = props; // store hooks const { instance: instanceStore } = useApplication(); - // toast - const { setToastAlert } = useToast(); // form data const { handleSubmit, @@ -42,9 +39,9 @@ export const InstanceGeneralForm: FC = (props) => { await instanceStore .updateInstanceInfo(payload) .then(() => - setToastAlert({ + setToast({ title: "Success", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Settings updated successfully", }) ) diff --git a/web/components/instance/github-config-form.tsx b/web/components/instance/github-config-form.tsx index 75639f82b..20fb08aff 100644 --- a/web/components/instance/github-config-form.tsx +++ b/web/components/instance/github-config-form.tsx @@ -2,12 +2,11 @@ import { FC, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { Copy, Eye, EyeOff } from "lucide-react"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IFormattedInstanceConfiguration } from "@plane/types"; // hooks import { useApplication } from "hooks/store"; -import useToast from "hooks/use-toast"; export interface IInstanceGithubConfigForm { config: IFormattedInstanceConfiguration; @@ -24,8 +23,6 @@ export const InstanceGithubConfigForm: FC = (props) = const [showPassword, setShowPassword] = useState(false); // store hooks const { instance: instanceStore } = useApplication(); - // toast - const { setToastAlert } = useToast(); // form data const { handleSubmit, @@ -44,9 +41,9 @@ export const InstanceGithubConfigForm: FC = (props) = await instanceStore .updateInstanceConfigurations(payload) .then(() => - setToastAlert({ + setToast({ title: "Success", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Github Configuration Settings updated successfully", }) ) @@ -145,9 +142,9 @@ export const InstanceGithubConfigForm: FC = (props) = className="flex items-center justify-between py-2" onClick={() => { navigator.clipboard.writeText(originURL); - setToastAlert({ + setToast({ message: "The Origin URL has been successfully copied to your clipboard", - type: "success", + type: TOAST_TYPE.SUCCESS, title: "Copied to clipboard", }); }} diff --git a/web/components/instance/google-config-form.tsx b/web/components/instance/google-config-form.tsx index cd7c3ab7d..27d4f4300 100644 --- a/web/components/instance/google-config-form.tsx +++ b/web/components/instance/google-config-form.tsx @@ -2,12 +2,11 @@ import { FC } from "react"; import { Controller, useForm } from "react-hook-form"; import { Copy } from "lucide-react"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IFormattedInstanceConfiguration } from "@plane/types"; // hooks import { useApplication } from "hooks/store"; -import useToast from "hooks/use-toast"; export interface IInstanceGoogleConfigForm { config: IFormattedInstanceConfiguration; @@ -22,8 +21,6 @@ export const InstanceGoogleConfigForm: FC = (props) = const { config } = props; // store hooks const { instance: instanceStore } = useApplication(); - // toast - const { setToastAlert } = useToast(); // form data const { handleSubmit, @@ -42,9 +39,9 @@ export const InstanceGoogleConfigForm: FC = (props) = await instanceStore .updateInstanceConfigurations(payload) .then(() => - setToastAlert({ + setToast({ title: "Success", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Google Configuration Settings updated successfully", }) ) @@ -94,9 +91,9 @@ export const InstanceGoogleConfigForm: FC = (props) = className="flex items-center justify-between py-2" onClick={() => { navigator.clipboard.writeText(originURL); - setToastAlert({ + setToast({ message: "The Origin URL has been successfully copied to your clipboard", - type: "success", + type: TOAST_TYPE.SUCCESS, title: "Copied to clipboard", }); }} diff --git a/web/components/instance/image-config-form.tsx b/web/components/instance/image-config-form.tsx index 93ce88719..26694c4ec 100644 --- a/web/components/instance/image-config-form.tsx +++ b/web/components/instance/image-config-form.tsx @@ -2,12 +2,11 @@ import { FC, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { Eye, EyeOff } from "lucide-react"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IFormattedInstanceConfiguration } from "@plane/types"; // hooks import { useApplication } from "hooks/store"; -import useToast from "hooks/use-toast"; export interface IInstanceImageConfigForm { config: IFormattedInstanceConfiguration; @@ -23,8 +22,6 @@ export const InstanceImageConfigForm: FC = (props) => const [showPassword, setShowPassword] = useState(false); // store hooks const { instance: instanceStore } = useApplication(); - // toast - const { setToastAlert } = useToast(); // form data const { handleSubmit, @@ -42,9 +39,9 @@ export const InstanceImageConfigForm: FC = (props) => await instanceStore .updateInstanceConfigurations(payload) .then(() => - setToastAlert({ + setToast({ title: "Success", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Image Configuration Settings updated successfully", }) ) diff --git a/web/components/instance/setup-form/sign-in-form.tsx b/web/components/instance/setup-form/sign-in-form.tsx index c4a9de6a3..174c66e82 100644 --- a/web/components/instance/setup-form/sign-in-form.tsx +++ b/web/components/instance/setup-form/sign-in-form.tsx @@ -4,12 +4,10 @@ import { Eye, EyeOff, XCircle } from "lucide-react"; // hooks import { useUser } from "hooks/store"; // ui -import { Input, Button } from "@plane/ui"; +import { Input, Button, TOAST_TYPE, setToast } from "@plane/ui"; // services import { AuthService } from "services/auth.service"; const authService = new AuthService(); -// hooks -import useToast from "hooks/use-toast"; // helpers import { checkEmailValidity } from "helpers/string.helper"; @@ -40,8 +38,6 @@ export const InstanceSetupSignInForm: FC = (props) => { password: "", }, }); - // hooks - const { setToastAlert } = useToast(); const handleFormSubmit = async (formValues: InstanceSetupEmailFormValues) => { const payload = { @@ -56,8 +52,8 @@ export const InstanceSetupSignInForm: FC = (props) => { handleNextStep(formValues.email); }) .catch((err) => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }); diff --git a/web/components/instance/sidebar-dropdown.tsx b/web/components/instance/sidebar-dropdown.tsx index 2bac426d6..e763663d2 100644 --- a/web/components/instance/sidebar-dropdown.tsx +++ b/web/components/instance/sidebar-dropdown.tsx @@ -10,10 +10,8 @@ import { Menu, Transition } from "@headlessui/react"; import { LogIn, LogOut, Settings, UserCog2 } from "lucide-react"; // hooks import { useApplication, useUser } from "hooks/store"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Avatar, Tooltip } from "@plane/ui"; +import { Avatar, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // Static Data const PROFILE_LINKS = [ @@ -35,7 +33,6 @@ export const InstanceSidebarDropdown = observer(() => { } = useApplication(); const { signOut, currentUser, currentUserSettings } = useUser(); // hooks - const { setToastAlert } = useToast(); const { setTheme } = useTheme(); // redirect url for normal mode @@ -53,8 +50,8 @@ export const InstanceSidebarDropdown = observer(() => { router.push("/"); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Failed to sign out. Please try again.", }) diff --git a/web/components/integration/delete-import-modal.tsx b/web/components/integration/delete-import-modal.tsx index bc1351125..ee9fadaa0 100644 --- a/web/components/integration/delete-import-modal.tsx +++ b/web/components/integration/delete-import-modal.tsx @@ -8,10 +8,8 @@ import { mutate } from "swr"; import { Dialog, Transition } from "@headlessui/react"; // services import { IntegrationService } from "services/integrations/integration.service"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { AlertTriangle } from "lucide-react"; // types @@ -36,8 +34,6 @@ export const DeleteImportModal: React.FC = ({ isOpen, handleClose, data } const router = useRouter(); const { workspaceSlug } = router.query; - const { setToastAlert } = useToast(); - const handleDeletion = () => { if (!workspaceSlug || !data) return; @@ -52,8 +48,8 @@ export const DeleteImportModal: React.FC = ({ isOpen, handleClose, data } integrationService .deleteImporterService(workspaceSlug as string, data.service, data.id) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong. Please try again.", }) diff --git a/web/components/integration/github/root.tsx b/web/components/integration/github/root.tsx index bc577328a..f26f651fe 100644 --- a/web/components/integration/github/root.tsx +++ b/web/components/integration/github/root.tsx @@ -10,8 +10,6 @@ import useSWR, { mutate } from "swr"; import { useForm } from "react-hook-form"; // services import { IntegrationService, GithubIntegrationService } from "services/integrations"; -// hooks -import useToast from "hooks/use-toast"; // components import { GithubImportConfigure, @@ -21,7 +19,7 @@ import { GithubImportConfirm, } from "components/integration"; // icons -import { UserGroupIcon } from "@plane/ui"; +import { UserGroupIcon, TOAST_TYPE, setToast } from "@plane/ui"; import { ArrowLeft, Check, List, Settings, UploadCloud } from "lucide-react"; // images import GithubLogo from "public/services/github.png"; @@ -92,8 +90,6 @@ export const GithubImporterRoot: React.FC = () => { const router = useRouter(); const { workspaceSlug, provider } = router.query; - const { setToastAlert } = useToast(); - const { handleSubmit, control, setValue, watch } = useForm({ defaultValues: defaultFormValues, }); @@ -149,8 +145,8 @@ export const GithubImporterRoot: React.FC = () => { mutate(IMPORTER_SERVICES_LIST(workspaceSlug as string)); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Import was unsuccessful. Please try again.", }) diff --git a/web/components/integration/single-integration-card.tsx b/web/components/integration/single-integration-card.tsx index 3026d6981..b36082b67 100644 --- a/web/components/integration/single-integration-card.tsx +++ b/web/components/integration/single-integration-card.tsx @@ -8,10 +8,9 @@ import useSWR, { mutate } from "swr"; import { IntegrationService } from "services/integrations"; // hooks import { useApplication, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; import useIntegrationPopup from "hooks/use-integration-popup"; // ui -import { Button, Loader, Tooltip } from "@plane/ui"; +import { Button, Loader, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // icons import GithubLogo from "public/services/github.png"; import SlackLogo from "public/services/slack.png"; @@ -54,8 +53,6 @@ export const SingleIntegrationCard: React.FC = observer(({ integration }) const { membership: { currentWorkspaceRole }, } = useUser(); - // toast alert - const { setToastAlert } = useToast(); const isUserAdmin = currentWorkspaceRole === 20; @@ -87,8 +84,8 @@ export const SingleIntegrationCard: React.FC = observer(({ integration }) ); setDeletingIntegration(false); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Deleted successfully!", message: `${integration.title} integration deleted successfully.`, }); @@ -96,8 +93,8 @@ export const SingleIntegrationCard: React.FC = observer(({ integration }) .catch(() => { setDeletingIntegration(false); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: `${integration.title} integration could not be deleted. Please try again.`, }); diff --git a/web/components/issues/archive-issue-modal.tsx b/web/components/issues/archive-issue-modal.tsx index 94c4c801a..032077957 100644 --- a/web/components/issues/archive-issue-modal.tsx +++ b/web/components/issues/archive-issue-modal.tsx @@ -3,9 +3,8 @@ import { Dialog, Transition } from "@headlessui/react"; // hooks import { useProject } from "hooks/store"; import { useIssues } from "hooks/store/use-issues"; -import useToast from "hooks/use-toast"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import { TIssue } from "@plane/types"; @@ -24,8 +23,6 @@ export const ArchiveIssueModal: React.FC = (props) => { // store hooks const { getProjectById } = useProject(); const { issueMap } = useIssues(); - // toast alert - const { setToastAlert } = useToast(); if (!dataId && !data) return null; @@ -44,8 +41,8 @@ export const ArchiveIssueModal: React.FC = (props) => { await onSubmit() .then(() => onClose()) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Issue could not be archived. Please try again.", }) diff --git a/web/components/issues/attachment/root.tsx b/web/components/issues/attachment/root.tsx index ffa17d337..fa3d0c220 100644 --- a/web/components/issues/attachment/root.tsx +++ b/web/components/issues/attachment/root.tsx @@ -1,7 +1,8 @@ import { FC, useMemo } from "react"; // hooks import { useEventTracker, useIssueDetail } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; // components import { IssueAttachmentUpload } from "./attachment-upload"; import { IssueAttachmentsList } from "./attachments-list"; @@ -24,19 +25,27 @@ export const IssueAttachmentRoot: FC = (props) => { // hooks const { createAttachment, removeAttachment } = useIssueDetail(); const { captureIssueEvent } = useEventTracker(); - const { setToastAlert } = useToast(); const handleAttachmentOperations: TAttachmentOperations = useMemo( () => ({ create: async (data: FormData) => { try { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields"); - const res = await createAttachment(workspaceSlug, projectId, issueId, data); - setToastAlert({ - message: "The attachment has been successfully uploaded", - type: "success", - title: "Attachment uploaded", + + const attachmentUploadPromise = createAttachment(workspaceSlug, projectId, issueId, data); + setPromiseToast(attachmentUploadPromise, { + loading: "Uploading attachment...", + success: { + title: "Attachment uploaded", + message: () => "The attachment has been successfully uploaded", + }, + error: { + title: "Attachment not uploaded", + message: () => "The attachment could not be uploaded", + }, }); + + const res = await attachmentUploadPromise; captureIssueEvent({ eventName: "Issue attachment added", payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, @@ -50,20 +59,15 @@ export const IssueAttachmentRoot: FC = (props) => { eventName: "Issue attachment added", payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, }); - setToastAlert({ - message: "The attachment could not be uploaded", - type: "error", - title: "Attachment not uploaded", - }); } }, remove: async (attachmentId: string) => { try { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields"); await removeAttachment(workspaceSlug, projectId, issueId, attachmentId); - setToastAlert({ + setToast({ message: "The attachment has been successfully removed", - type: "success", + type: TOAST_TYPE.SUCCESS, title: "Attachment removed", }); captureIssueEvent({ @@ -83,15 +87,15 @@ export const IssueAttachmentRoot: FC = (props) => { change_details: "", }, }); - setToastAlert({ + setToast({ message: "The Attachment could not be removed", - type: "error", + type: TOAST_TYPE.ERROR, title: "Attachment not removed", }); } }, }), - [workspaceSlug, projectId, issueId, createAttachment, removeAttachment, setToastAlert] + [workspaceSlug, projectId, issueId, createAttachment, removeAttachment] ); return ( diff --git a/web/components/issues/delete-issue-modal.tsx b/web/components/issues/delete-issue-modal.tsx index 3a9c0653e..b8e6b3f85 100644 --- a/web/components/issues/delete-issue-modal.tsx +++ b/web/components/issues/delete-issue-modal.tsx @@ -2,9 +2,7 @@ import { useEffect, useState, Fragment } from "react"; import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; // ui -import { Button } from "@plane/ui"; -// hooks -import useToast from "hooks/use-toast"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import { useIssues } from "hooks/store/use-issues"; import { TIssue } from "@plane/types"; @@ -25,7 +23,6 @@ export const DeleteIssueModal: React.FC = (props) => { const [isDeleting, setIsDeleting] = useState(false); - const { setToastAlert } = useToast(); // hooks const { getProjectById } = useProject(); @@ -50,9 +47,9 @@ export const DeleteIssueModal: React.FC = (props) => { onClose(); }) .catch(() => { - setToastAlert({ + setToast({ title: "Error", - type: "error", + type: TOAST_TYPE.ERROR, message: "Failed to delete issue", }); }) diff --git a/web/components/issues/description-form.tsx b/web/components/issues/description-form.tsx index c64c147ea..1ee22a6cb 100644 --- a/web/components/issues/description-form.tsx +++ b/web/components/issues/description-form.tsx @@ -71,16 +71,10 @@ export const IssueDescriptionForm: FC = observer((props) => { async (formData: Partial) => { if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return; - await issueOperations.update( - workspaceSlug, - projectId, - issueId, - { - name: formData.name ?? "", - description_html: formData.description_html ?? "

    ", - }, - false - ); + await issueOperations.update(workspaceSlug, projectId, issueId, { + name: formData.name ?? "", + description_html: formData.description_html ?? "

    ", + }); }, [workspaceSlug, projectId, issueId, issueOperations] ); diff --git a/web/components/issues/description-input.tsx b/web/components/issues/description-input.tsx index 79634fa84..65e82df5f 100644 --- a/web/components/issues/description-input.tsx +++ b/web/components/issues/description-input.tsx @@ -41,11 +41,9 @@ export const IssueDescriptionInput: FC = (props) => useEffect(() => { if (debouncedValue && debouncedValue !== value) { - issueOperations - .update(workspaceSlug, projectId, issueId, { description_html: debouncedValue }, false) - .finally(() => { - setIsSubmitting("submitted"); - }); + issueOperations.update(workspaceSlug, projectId, issueId, { description_html: debouncedValue }).finally(() => { + setIsSubmitting("submitted"); + }); } // DO NOT Add more dependencies here. It will cause multiple requests to be sent. // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/web/components/issues/issue-detail/cycle-select.tsx b/web/components/issues/issue-detail/cycle-select.tsx index 84dccefac..4da762a9d 100644 --- a/web/components/issues/issue-detail/cycle-select.tsx +++ b/web/components/issues/issue-detail/cycle-select.tsx @@ -56,7 +56,6 @@ export const IssueCycleSelect: React.FC = observer((props) => dropdownArrow dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline" /> - {isUpdating && }
    ); }); diff --git a/web/components/issues/issue-detail/inbox/root.tsx b/web/components/issues/issue-detail/inbox/root.tsx index d96b36efa..9b0e961c0 100644 --- a/web/components/issues/issue-detail/inbox/root.tsx +++ b/web/components/issues/issue-detail/inbox/root.tsx @@ -6,7 +6,8 @@ import { InboxIssueMainContent } from "./main-content"; import { InboxIssueDetailsSidebar } from "./sidebar"; // hooks import { useEventTracker, useInboxIssues, useIssueDetail, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // types import { TIssue } from "@plane/types"; import { TIssueOperations } from "../root"; @@ -34,7 +35,6 @@ export const InboxIssueDetailRoot: FC = (props) => { fetchComments, } = useIssueDetail(); const { captureIssueEvent } = useEventTracker(); - const { setToastAlert } = useToast(); const { membership: { currentProjectRole }, } = useUser(); @@ -53,17 +53,9 @@ export const InboxIssueDetailRoot: FC = (props) => { projectId: string, issueId: string, data: Partial, - showToast: boolean = true ) => { try { await updateInboxIssue(workspaceSlug, projectId, inboxId, issueId, data); - if (showToast) { - setToastAlert({ - title: "Issue updated successfully", - type: "success", - message: "Issue updated successfully", - }); - } captureIssueEvent({ eventName: "Inbox issue updated", payload: { ...data, state: "SUCCESS", element: "Inbox" }, @@ -74,9 +66,9 @@ export const InboxIssueDetailRoot: FC = (props) => { path: router.asPath, }); } catch (error) { - setToastAlert({ + setToast({ title: "Issue update failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Issue update failed", }); captureIssueEvent({ @@ -93,9 +85,9 @@ export const InboxIssueDetailRoot: FC = (props) => { remove: async (workspaceSlug: string, projectId: string, issueId: string) => { try { await removeInboxIssue(workspaceSlug, projectId, inboxId, issueId); - setToastAlert({ + setToast({ title: "Issue deleted successfully", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Issue deleted successfully", }); captureIssueEvent({ @@ -109,15 +101,15 @@ export const InboxIssueDetailRoot: FC = (props) => { payload: { id: issueId, state: "FAILED", element: "Inbox" }, path: router.asPath, }); - setToastAlert({ + setToast({ title: "Issue delete failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Issue delete failed", }); } }, }), - [inboxId, fetchInboxIssueById, updateInboxIssue, removeInboxIssue, setToastAlert] + [inboxId, fetchInboxIssueById, updateInboxIssue, removeInboxIssue] ); useSWR( diff --git a/web/components/issues/issue-detail/issue-activity/root.tsx b/web/components/issues/issue-detail/issue-activity/root.tsx index 695b248de..673e6c2af 100644 --- a/web/components/issues/issue-detail/issue-activity/root.tsx +++ b/web/components/issues/issue-detail/issue-activity/root.tsx @@ -3,7 +3,8 @@ import { observer } from "mobx-react-lite"; import { History, LucideIcon, MessageCircle, ListRestart } from "lucide-react"; // hooks import { useIssueDetail, useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { IssueActivityCommentRoot, IssueActivityRoot, IssueCommentRoot, IssueCommentCreate } from "./"; // types @@ -45,7 +46,6 @@ export const IssueActivity: FC = observer((props) => { const { workspaceSlug, projectId, issueId } = props; // hooks const { createComment, updateComment, removeComment } = useIssueDetail(); - const { setToastAlert } = useToast(); const { getProjectById } = useProject(); // state const [activityTab, setActivityTab] = useState("all"); @@ -56,15 +56,15 @@ export const IssueActivity: FC = observer((props) => { try { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields"); await createComment(workspaceSlug, projectId, issueId, data); - setToastAlert({ + setToast({ title: "Comment created successfully.", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Comment created successfully.", }); } catch (error) { - setToastAlert({ + setToast({ title: "Comment creation failed.", - type: "error", + type: TOAST_TYPE.ERROR, message: "Comment creation failed. Please try again later.", }); } @@ -73,15 +73,15 @@ export const IssueActivity: FC = observer((props) => { try { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields"); await updateComment(workspaceSlug, projectId, issueId, commentId, data); - setToastAlert({ + setToast({ title: "Comment updated successfully.", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Comment updated successfully.", }); } catch (error) { - setToastAlert({ + setToast({ title: "Comment update failed.", - type: "error", + type: TOAST_TYPE.ERROR, message: "Comment update failed. Please try again later.", }); } @@ -90,21 +90,21 @@ export const IssueActivity: FC = observer((props) => { try { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields"); await removeComment(workspaceSlug, projectId, issueId, commentId); - setToastAlert({ + setToast({ title: "Comment removed successfully.", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Comment removed successfully.", }); } catch (error) { - setToastAlert({ + setToast({ title: "Comment remove failed.", - type: "error", + type: TOAST_TYPE.ERROR, message: "Comment remove failed. Please try again later.", }); } }, }), - [workspaceSlug, projectId, issueId, createComment, updateComment, removeComment, setToastAlert] + [workspaceSlug, projectId, issueId, createComment, updateComment, removeComment] ); const project = getProjectById(projectId); diff --git a/web/components/issues/issue-detail/label/create-label.tsx b/web/components/issues/issue-detail/label/create-label.tsx index 72bc034f8..8a6eea17e 100644 --- a/web/components/issues/issue-detail/label/create-label.tsx +++ b/web/components/issues/issue-detail/label/create-label.tsx @@ -5,9 +5,8 @@ import { TwitterPicker } from "react-color"; import { Popover, Transition } from "@headlessui/react"; // hooks import { useIssueDetail } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Input } from "@plane/ui"; +import { Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import { TLabelOperations } from "./root"; import { IIssueLabel } from "@plane/types"; @@ -28,7 +27,6 @@ const defaultValues: Partial = { export const LabelCreate: FC = (props) => { const { workspaceSlug, projectId, issueId, labelOperations, disabled = false } = props; // hooks - const { setToastAlert } = useToast(); const { issue: { getIssueById }, } = useIssueDetail(); @@ -63,9 +61,9 @@ export const LabelCreate: FC = (props) => { await labelOperations.updateIssue(workspaceSlug, projectId, issueId, { label_ids: currentLabels }); reset(defaultValues); } catch (error) { - setToastAlert({ + setToast({ title: "Label creation failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Label creation failed. Please try again sometime later.", }); } diff --git a/web/components/issues/issue-detail/label/root.tsx b/web/components/issues/issue-detail/label/root.tsx index 94f9b451f..59ce1f54c 100644 --- a/web/components/issues/issue-detail/label/root.tsx +++ b/web/components/issues/issue-detail/label/root.tsx @@ -4,9 +4,10 @@ import { observer } from "mobx-react-lite"; import { LabelList, LabelCreate, IssueLabelSelectRoot } from "./"; // hooks import { useIssueDetail, useLabel } from "hooks/store"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // types import { IIssueLabel, TIssue } from "@plane/types"; -import useToast from "hooks/use-toast"; export type TIssueLabel = { workspaceSlug: string; @@ -27,7 +28,6 @@ export const IssueLabel: FC = observer((props) => { // hooks const { updateIssue } = useIssueDetail(); const { createLabel } = useLabel(); - const { setToastAlert } = useToast(); const labelOperations: TLabelOperations = useMemo( () => ({ @@ -35,16 +35,10 @@ export const IssueLabel: FC = observer((props) => { try { if (onLabelUpdate) onLabelUpdate(data.label_ids || []); else await updateIssue(workspaceSlug, projectId, issueId, data); - if (!isInboxIssue) - setToastAlert({ - title: "Issue updated successfully", - type: "success", - message: "Issue updated successfully", - }); } catch (error) { - setToastAlert({ + setToast({ title: "Issue update failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Issue update failed", }); } @@ -53,23 +47,23 @@ export const IssueLabel: FC = observer((props) => { try { const labelResponse = await createLabel(workspaceSlug, projectId, data); if (!isInboxIssue) - setToastAlert({ + setToast({ title: "Label created successfully", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Label created successfully", }); return labelResponse; } catch (error) { - setToastAlert({ + setToast({ title: "Label creation failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Label creation failed", }); return error; } }, }), - [updateIssue, createLabel, setToastAlert, onLabelUpdate] + [updateIssue, createLabel, onLabelUpdate] ); return ( diff --git a/web/components/issues/issue-detail/links/link-detail.tsx b/web/components/issues/issue-detail/links/link-detail.tsx index 6c37f86f9..f1b003b99 100644 --- a/web/components/issues/issue-detail/links/link-detail.tsx +++ b/web/components/issues/issue-detail/links/link-detail.tsx @@ -1,9 +1,8 @@ import { FC, useState } from "react"; // hooks -import useToast from "hooks/use-toast"; import { useIssueDetail, useMember } from "hooks/store"; // ui -import { ExternalLinkIcon, Tooltip } from "@plane/ui"; +import { ExternalLinkIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { Pencil, Trash2, LinkIcon } from "lucide-react"; // types @@ -27,7 +26,6 @@ export const IssueLinkDetail: FC = (props) => { link: { getLinkById }, } = useIssueDetail(); const { getUserDetails } = useMember(); - const { setToastAlert } = useToast(); // state const [isIssueLinkModalOpen, setIsIssueLinkModalOpen] = useState(false); @@ -55,8 +53,8 @@ export const IssueLinkDetail: FC = (props) => { className="flex w-full items-start justify-between gap-2 cursor-pointer" onClick={() => { copyTextToClipboard(linkDetail.url); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link copied!", message: "Link copied to clipboard", }); diff --git a/web/components/issues/issue-detail/links/root.tsx b/web/components/issues/issue-detail/links/root.tsx index 94124085a..672a9e35e 100644 --- a/web/components/issues/issue-detail/links/root.tsx +++ b/web/components/issues/issue-detail/links/root.tsx @@ -2,7 +2,8 @@ import { FC, useCallback, useMemo, useState } from "react"; import { Plus } from "lucide-react"; // hooks import { useIssueDetail } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { IssueLinkCreateUpdateModal } from "./create-update-link-modal"; import { IssueLinkList } from "./links"; @@ -37,24 +38,22 @@ export const IssueLinkRoot: FC = (props) => { [toggleIssueLinkModalStore] ); - const { setToastAlert } = useToast(); - const handleLinkOperations: TLinkOperations = useMemo( () => ({ create: async (data: Partial) => { try { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields"); await createLink(workspaceSlug, projectId, issueId, data); - setToastAlert({ + setToast({ message: "The link has been successfully created", - type: "success", + type: TOAST_TYPE.SUCCESS, title: "Link created", }); toggleIssueLinkModal(false); } catch (error) { - setToastAlert({ + setToast({ message: "The link could not be created", - type: "error", + type: TOAST_TYPE.ERROR, title: "Link not created", }); } @@ -63,16 +62,16 @@ export const IssueLinkRoot: FC = (props) => { try { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields"); await updateLink(workspaceSlug, projectId, issueId, linkId, data); - setToastAlert({ + setToast({ message: "The link has been successfully updated", - type: "success", + type: TOAST_TYPE.SUCCESS, title: "Link updated", }); toggleIssueLinkModal(false); } catch (error) { - setToastAlert({ + setToast({ message: "The link could not be updated", - type: "error", + type: TOAST_TYPE.ERROR, title: "Link not updated", }); } @@ -81,22 +80,22 @@ export const IssueLinkRoot: FC = (props) => { try { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields"); await removeLink(workspaceSlug, projectId, issueId, linkId); - setToastAlert({ + setToast({ message: "The link has been successfully removed", - type: "success", + type: TOAST_TYPE.SUCCESS, title: "Link removed", }); toggleIssueLinkModal(false); } catch (error) { - setToastAlert({ + setToast({ message: "The link could not be removed", - type: "error", + type: TOAST_TYPE.ERROR, title: "Link not removed", }); } }, }), - [workspaceSlug, projectId, issueId, createLink, updateLink, removeLink, setToastAlert, toggleIssueLinkModal] + [workspaceSlug, projectId, issueId, createLink, updateLink, removeLink, toggleIssueLinkModal] ); return ( diff --git a/web/components/issues/issue-detail/module-select.tsx b/web/components/issues/issue-detail/module-select.tsx index 41f4a06d6..f0fe06a2e 100644 --- a/web/components/issues/issue-detail/module-select.tsx +++ b/web/components/issues/issue-detail/module-select.tsx @@ -75,7 +75,6 @@ export const IssueModuleSelect: React.FC = observer((props) showTooltip multiple /> - {isUpdating && }
    ); }); diff --git a/web/components/issues/issue-detail/reactions/issue-comment.tsx b/web/components/issues/issue-detail/reactions/issue-comment.tsx index 30a8621e4..2268540bf 100644 --- a/web/components/issues/issue-detail/reactions/issue-comment.tsx +++ b/web/components/issues/issue-detail/reactions/issue-comment.tsx @@ -4,7 +4,8 @@ import { observer } from "mobx-react-lite"; import { ReactionSelector } from "./reaction-selector"; // hooks import { useIssueDetail } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // types import { IUser } from "@plane/types"; import { renderEmoji } from "helpers/emoji.helper"; @@ -25,7 +26,6 @@ export const IssueCommentReaction: FC = observer((props) createCommentReaction, removeCommentReaction, } = useIssueDetail(); - const { setToastAlert } = useToast(); const reactionIds = getCommentReactionsByCommentId(commentId); const userReactions = commentReactionsByUser(commentId, currentUser.id).map((r) => r.reaction); @@ -36,15 +36,15 @@ export const IssueCommentReaction: FC = observer((props) try { if (!workspaceSlug || !projectId || !commentId) throw new Error("Missing fields"); await createCommentReaction(workspaceSlug, projectId, commentId, reaction); - setToastAlert({ + setToast({ title: "Reaction created successfully", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Reaction created successfully", }); } catch (error) { - setToastAlert({ + setToast({ title: "Reaction creation failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Reaction creation failed", }); } @@ -53,15 +53,15 @@ export const IssueCommentReaction: FC = observer((props) try { if (!workspaceSlug || !projectId || !commentId || !currentUser?.id) throw new Error("Missing fields"); removeCommentReaction(workspaceSlug, projectId, commentId, reaction, currentUser.id); - setToastAlert({ + setToast({ title: "Reaction removed successfully", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Reaction removed successfully", }); } catch (error) { - setToastAlert({ + setToast({ title: "Reaction remove failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Reaction remove failed", }); } @@ -78,7 +78,6 @@ export const IssueCommentReaction: FC = observer((props) currentUser, createCommentReaction, removeCommentReaction, - setToastAlert, userReactions, ] ); diff --git a/web/components/issues/issue-detail/reactions/issue.tsx b/web/components/issues/issue-detail/reactions/issue.tsx index d6b33e36b..a9bc264f3 100644 --- a/web/components/issues/issue-detail/reactions/issue.tsx +++ b/web/components/issues/issue-detail/reactions/issue.tsx @@ -4,7 +4,8 @@ import { observer } from "mobx-react-lite"; import { ReactionSelector } from "./reaction-selector"; // hooks import { useIssueDetail } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // types import { IUser } from "@plane/types"; import { renderEmoji } from "helpers/emoji.helper"; @@ -24,7 +25,6 @@ export const IssueReaction: FC = observer((props) => { createReaction, removeReaction, } = useIssueDetail(); - const { setToastAlert } = useToast(); const reactionIds = getReactionsByIssueId(issueId); const userReactions = reactionsByUser(issueId, currentUser.id).map((r) => r.reaction); @@ -35,15 +35,15 @@ export const IssueReaction: FC = observer((props) => { try { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields"); await createReaction(workspaceSlug, projectId, issueId, reaction); - setToastAlert({ + setToast({ title: "Reaction created successfully", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Reaction created successfully", }); } catch (error) { - setToastAlert({ + setToast({ title: "Reaction creation failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Reaction creation failed", }); } @@ -52,15 +52,15 @@ export const IssueReaction: FC = observer((props) => { try { if (!workspaceSlug || !projectId || !issueId || !currentUser?.id) throw new Error("Missing fields"); await removeReaction(workspaceSlug, projectId, issueId, reaction, currentUser.id); - setToastAlert({ + setToast({ title: "Reaction removed successfully", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Reaction removed successfully", }); } catch (error) { - setToastAlert({ + setToast({ title: "Reaction remove failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Reaction remove failed", }); } @@ -70,7 +70,7 @@ export const IssueReaction: FC = observer((props) => { else await issueReactionOperations.create(reaction); }, }), - [workspaceSlug, projectId, issueId, currentUser, createReaction, removeReaction, setToastAlert, userReactions] + [workspaceSlug, projectId, issueId, currentUser, createReaction, removeReaction, userReactions] ); return ( diff --git a/web/components/issues/issue-detail/relation-select.tsx b/web/components/issues/issue-detail/relation-select.tsx index 67bba8697..260377406 100644 --- a/web/components/issues/issue-detail/relation-select.tsx +++ b/web/components/issues/issue-detail/relation-select.tsx @@ -4,11 +4,10 @@ import { observer } from "mobx-react-lite"; import { CircleDot, CopyPlus, Pencil, X, XCircle } from "lucide-react"; // hooks import { useIssueDetail, useIssues, useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { ExistingIssuesListModal } from "components/core"; // ui -import { RelatedIcon, Tooltip } from "@plane/ui"; +import { RelatedIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { cn } from "helpers/common.helper"; // types @@ -60,15 +59,13 @@ export const IssueRelationSelect: React.FC = observer((pro toggleRelationModal, } = useIssueDetail(); const { issueMap } = useIssues(); - // toast alert - const { setToastAlert } = useToast(); const relationIssueIds = getRelationByIssueIdRelationType(issueId, relationKey); const onSubmit = async (data: ISearchIssueResponse[]) => { if (data.length === 0) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please select at least one issue.", }); diff --git a/web/components/issues/issue-detail/root.tsx b/web/components/issues/issue-detail/root.tsx index 0e343d9a8..be0fbb74d 100644 --- a/web/components/issues/issue-detail/root.tsx +++ b/web/components/issues/issue-detail/root.tsx @@ -10,9 +10,10 @@ import { EmptyState } from "components/common"; import emptyIssue from "public/empty-state/issue.svg"; // hooks import { useApplication, useEventTracker, useIssueDetail, useIssues, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // types import { TIssue } from "@plane/types"; +// ui +import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; // constants import { EUserProjectRoles } from "constants/project"; import { EIssuesStoreType } from "constants/issue"; @@ -21,13 +22,7 @@ import { observer } from "mobx-react"; export type TIssueOperations = { fetch: (workspaceSlug: string, projectId: string, issueId: string) => Promise; - update: ( - workspaceSlug: string, - projectId: string, - issueId: string, - data: Partial, - showToast?: boolean - ) => Promise; + update: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise; archive?: (workspaceSlug: string, projectId: string, issueId: string) => Promise; restore?: (workspaceSlug: string, projectId: string, issueId: string) => Promise; @@ -76,7 +71,6 @@ export const IssueDetailRoot: FC = observer((props) => { issues: { removeIssue: removeArchivedIssue }, } = useIssues(EIssuesStoreType.ARCHIVED); const { captureIssueEvent } = useEventTracker(); - const { setToastAlert } = useToast(); const { membership: { currentProjectRole }, } = useUser(); @@ -91,22 +85,9 @@ export const IssueDetailRoot: FC = observer((props) => { console.error("Error fetching the parent issue"); } }, - update: async ( - workspaceSlug: string, - projectId: string, - issueId: string, - data: Partial, - showToast: boolean = true - ) => { + update: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => { try { await updateIssue(workspaceSlug, projectId, issueId, data); - if (showToast) { - setToastAlert({ - title: "Issue updated successfully", - type: "success", - message: "Issue updated successfully", - }); - } captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { ...data, issueId, state: "SUCCESS", element: "Issue detail page" }, @@ -126,9 +107,9 @@ export const IssueDetailRoot: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ + setToast({ title: "Issue update failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Issue update failed", }); } @@ -138,9 +119,9 @@ export const IssueDetailRoot: FC = observer((props) => { let response; if (is_archived) response = await removeArchivedIssue(workspaceSlug, projectId, issueId); else response = await removeIssue(workspaceSlug, projectId, issueId); - setToastAlert({ + setToast({ title: "Issue deleted successfully", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Issue deleted successfully", }); captureIssueEvent({ @@ -149,9 +130,9 @@ export const IssueDetailRoot: FC = observer((props) => { path: router.asPath, }); } catch (error) { - setToastAlert({ + setToast({ title: "Issue delete failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Issue delete failed", }); captureIssueEvent({ @@ -164,8 +145,8 @@ export const IssueDetailRoot: FC = observer((props) => { archive: async (workspaceSlug: string, projectId: string, issueId: string) => { try { await archiveIssue(workspaceSlug, projectId, issueId); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Issue archived successfully.", }); @@ -175,8 +156,8 @@ export const IssueDetailRoot: FC = observer((props) => { path: router.asPath, }); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Issue could not be archived. Please try again.", }); @@ -189,12 +170,19 @@ export const IssueDetailRoot: FC = observer((props) => { }, addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => { try { - await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds); - setToastAlert({ - title: "Cycle added to issue successfully", - type: "success", - message: "Issue added to issue successfully", + const addToCyclePromise = addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds); + setPromiseToast(addToCyclePromise, { + loading: "Adding cycle to issue...", + success: { + title: "Success!", + message: () => "Cycle added to issue successfully", + }, + error: { + title: "Error!", + message: () => "Cycle add to issue failed", + }, }); + await addToCyclePromise; captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { ...issueIds, state: "SUCCESS", element: "Issue detail page" }, @@ -214,21 +202,23 @@ export const IssueDetailRoot: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ - title: "Cycle add to issue failed", - type: "error", - message: "Cycle add to issue failed", - }); } }, removeIssueFromCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => { try { - const response = await removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId); - setToastAlert({ - title: "Cycle removed from issue successfully", - type: "success", - message: "Cycle removed from issue successfully", + const removeFromCyclePromise = removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId); + setPromiseToast(removeFromCyclePromise, { + loading: "Removing cycle from issue...", + success: { + title: "Success!", + message: () => "Cycle removed from issue successfully", + }, + error: { + title: "Error!", + message: () => "Cycle remove from issue failed", + }, }); + const response = await removeFromCyclePromise; captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { ...response, state: "SUCCESS", element: "Issue detail page" }, @@ -248,21 +238,23 @@ export const IssueDetailRoot: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ - title: "Cycle remove from issue failed", - type: "error", - message: "Cycle remove from issue failed", - }); } }, addModulesToIssue: async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => { try { - const response = await addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds); - setToastAlert({ - title: "Module added to issue successfully", - type: "success", - message: "Module added to issue successfully", + const addToModulePromise = addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds); + setPromiseToast(addToModulePromise, { + loading: "Adding module to issue...", + success: { + title: "Success!", + message: () => "Module added to issue successfully", + }, + error: { + title: "Error!", + message: () => "Module add to issue failed", + }, }); + const response = await addToModulePromise; captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { ...response, state: "SUCCESS", element: "Issue detail page" }, @@ -282,21 +274,23 @@ export const IssueDetailRoot: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ - title: "Module add to issue failed", - type: "error", - message: "Module add to issue failed", - }); } }, removeIssueFromModule: async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => { try { - await removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId); - setToastAlert({ - title: "Module removed from issue successfully", - type: "success", - message: "Module removed from issue successfully", + const removeFromModulePromise = removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId); + setPromiseToast(removeFromModulePromise, { + loading: "Removing module from issue...", + success: { + title: "Success!", + message: () => "Module removed from issue successfully", + }, + error: { + title: "Error!", + message: () => "Module remove from issue failed", + }, }); + await removeFromModulePromise; captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, @@ -316,11 +310,6 @@ export const IssueDetailRoot: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ - title: "Module remove from issue failed", - type: "error", - message: "Module remove from issue failed", - }); } }, removeModulesFromIssue: async ( @@ -329,20 +318,19 @@ export const IssueDetailRoot: FC = observer((props) => { issueId: string, moduleIds: string[] ) => { - try { - await removeModulesFromIssue(workspaceSlug, projectId, issueId, moduleIds); - setToastAlert({ - type: "success", - title: "Successful!", - message: "Issue removed from module successfully.", - }); - } catch (error) { - setToastAlert({ - type: "error", + const removeModulesFromIssuePromise = removeModulesFromIssue(workspaceSlug, projectId, issueId, moduleIds); + setPromiseToast(removeModulesFromIssuePromise, { + loading: "Removing module from issue...", + success: { + title: "Success!", + message: () => "Module removed from issue successfully", + }, + error: { title: "Error!", - message: "Issue could not be removed from module. Please try again.", - }); - } + message: () => "Module remove from issue failed", + }, + }); + await removeModulesFromIssuePromise; }, }), [ @@ -357,7 +345,6 @@ export const IssueDetailRoot: FC = observer((props) => { addModulesToIssue, removeIssueFromModule, removeModulesFromIssue, - setToastAlert, ] ); diff --git a/web/components/issues/issue-detail/sidebar.tsx b/web/components/issues/issue-detail/sidebar.tsx index a65cb7f16..33dc4cdf5 100644 --- a/web/components/issues/issue-detail/sidebar.tsx +++ b/web/components/issues/issue-detail/sidebar.tsx @@ -16,7 +16,6 @@ import { } from "lucide-react"; // hooks import { useEstimate, useIssueDetail, useProject, useProjectState, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { DeleteIssueModal, @@ -30,8 +29,18 @@ import { } from "components/issues"; import { IssueSubscription } from "./subscription"; import { DateDropdown, EstimateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns"; -// icons -import { ArchiveIcon, ContrastIcon, DiceIcon, DoubleCircleIcon, RelatedIcon, Tooltip, UserGroupIcon } from "@plane/ui"; +// ui +import { + ArchiveIcon, + ContrastIcon, + DiceIcon, + DoubleCircleIcon, + RelatedIcon, + Tooltip, + UserGroupIcon, + TOAST_TYPE, + setToast, +} from "@plane/ui"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { copyTextToClipboard } from "helpers/string.helper"; @@ -61,7 +70,6 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { const { getProjectById } = useProject(); const { currentUser } = useUser(); const { areEstimatesEnabledForCurrentProject } = useEstimate(); - const { setToastAlert } = useToast(); const { issue: { getIssueById }, } = useIssueDetail(); @@ -73,8 +81,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { const handleCopyText = () => { const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Issue link copied to clipboard.", }); diff --git a/web/components/issues/issue-detail/subscription.tsx b/web/components/issues/issue-detail/subscription.tsx index 7321ef27f..f4025a2f3 100644 --- a/web/components/issues/issue-detail/subscription.tsx +++ b/web/components/issues/issue-detail/subscription.tsx @@ -2,10 +2,9 @@ import { Bell, BellOff } from "lucide-react"; import { observer } from "mobx-react-lite"; import { FC, useState } from "react"; // UI -import { Button, Loader } from "@plane/ui"; +import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; // hooks import { useIssueDetail } from "hooks/store"; -import useToast from "hooks/use-toast"; import isNil from "lodash/isNil"; export type TIssueSubscription = { @@ -22,7 +21,6 @@ export const IssueSubscription: FC = observer((props) => { createSubscription, removeSubscription, } = useIssueDetail(); - const { setToastAlert } = useToast(); // state const [loading, setLoading] = useState(false); @@ -33,16 +31,16 @@ export const IssueSubscription: FC = observer((props) => { try { if (isSubscribed) await removeSubscription(workspaceSlug, projectId, issueId); else await createSubscription(workspaceSlug, projectId, issueId); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: `Issue ${isSubscribed ? `unsubscribed` : `subscribed`} successfully.!`, message: `Issue ${isSubscribed ? `unsubscribed` : `subscribed`} successfully.!`, }); setLoading(false); } catch (error) { setLoading(false); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error", message: "Something went wrong. Please try again later.", }); diff --git a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx index 43f62e5be..fb3373a06 100644 --- a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx +++ b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx @@ -4,8 +4,8 @@ import { observer } from "mobx-react-lite"; import { DragDropContext, DropResult } from "@hello-pangea/dnd"; // components import { CalendarChart } from "components/issues"; -// hooks -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // types import { TGroupedIssues, TIssue } from "@plane/types"; import { IQuickActionProps } from "../list/list-view-types"; @@ -41,7 +41,6 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { const { workspaceSlug, projectId } = router.query; // hooks - const { setToastAlert } = useToast(); const { issueMap } = useIssues(); const { membership: { currentProjectRole }, @@ -73,9 +72,9 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { groupedIssueIds, viewId ).catch((err) => { - setToastAlert({ + setToast({ title: "Error", - type: "error", + type: TOAST_TYPE.ERROR, message: err.detail ?? "Failed to perform this action", }); }); diff --git a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx index 6db9323fa..c70b05c70 100644 --- a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx @@ -4,13 +4,14 @@ import { useForm } from "react-hook-form"; import { observer } from "mobx-react-lite"; // hooks import { useEventTracker, useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // helpers import { createIssuePayload } from "helpers/issue.helper"; // icons import { PlusIcon } from "lucide-react"; +// ui +import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; // types import { TIssue } from "@plane/types"; // constants @@ -71,8 +72,6 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { const ref = useRef(null); // states const [isOpen, setIsOpen] = useState(false); - // toast alert - const { setToastAlert } = useToast(); // derived values const projectDetail = projectId ? getProjectById(projectId.toString()) : null; @@ -102,13 +101,13 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { Object.keys(errors).forEach((key) => { const error = errors[key as keyof TIssue]; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error?.message?.toString() || "Some error occurred. Please try again.", }); }); - }, [errors, setToastAlert]); + }, [errors]); const onSubmitHandler = async (formData: TIssue) => { if (isSubmitting || !workspaceSlug || !projectId) return; @@ -120,39 +119,42 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { ...formData, }); - try { - quickAddCallback && - (await quickAddCallback( - workspaceSlug.toString(), - projectId.toString(), - { - ...payload, - }, - viewId - ).then((res) => { + if (quickAddCallback) { + const quickAddPromise = quickAddCallback( + workspaceSlug.toString(), + projectId.toString(), + { + ...payload, + }, + viewId + ); + setPromiseToast(quickAddPromise, { + loading: "Adding issue...", + success: { + title: "Success!", + message: () => "Issue created successfully.", + }, + error: { + title: "Error!", + message: (err) => err?.message || "Some error occurred. Please try again.", + }, + }); + + await quickAddPromise + .then((res) => { captureIssueEvent({ eventName: ISSUE_CREATED, payload: { ...res, state: "SUCCESS", element: "Calendar quick add" }, path: router.asPath, }); - })); - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue created successfully.", - }); - } catch (err: any) { - console.error(err); - captureIssueEvent({ - eventName: ISSUE_CREATED, - payload: { ...payload, state: "FAILED", element: "Calendar quick add" }, - path: router.asPath, - }); - setToastAlert({ - type: "error", - title: "Error!", - message: err?.message || "Some error occurred. Please try again.", - }); + }) + .catch(() => { + captureIssueEvent({ + eventName: ISSUE_CREATED, + payload: { ...payload, state: "FAILED", element: "Calendar quick add" }, + path: router.asPath, + }); + }); } }; diff --git a/web/components/issues/issue-layouts/empty-states/cycle.tsx b/web/components/issues/issue-layouts/empty-states/cycle.tsx index 4b7676173..b23b1998e 100644 --- a/web/components/issues/issue-layouts/empty-states/cycle.tsx +++ b/web/components/issues/issue-layouts/empty-states/cycle.tsx @@ -4,7 +4,8 @@ import { PlusIcon } from "lucide-react"; import { useTheme } from "next-themes"; // hooks import { useApplication, useEventTracker, useIssueDetail, useIssues, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { ExistingIssuesListModal } from "components/core"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; @@ -53,16 +54,14 @@ export const CycleEmptyState: React.FC = observer((props) => { currentUser, } = useUser(); - const { setToastAlert } = useToast(); - const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => { if (!workspaceSlug || !projectId || !cycleId) return; const issueIds = data.map((i) => i.id); await issues.addIssueToCycle(workspaceSlug.toString(), projectId, cycleId.toString(), issueIds).catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Selected issues could not be added to the cycle. Please try again.", }); diff --git a/web/components/issues/issue-layouts/empty-states/module.tsx b/web/components/issues/issue-layouts/empty-states/module.tsx index ef7ec729c..7a5c6f57f 100644 --- a/web/components/issues/issue-layouts/empty-states/module.tsx +++ b/web/components/issues/issue-layouts/empty-states/module.tsx @@ -4,7 +4,8 @@ import { PlusIcon } from "lucide-react"; import { useTheme } from "next-themes"; // hooks import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { ExistingIssuesListModal } from "components/core"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; @@ -51,8 +52,6 @@ export const ModuleEmptyState: React.FC = observer((props) => { membership: { currentProjectRole: userRole }, currentUser, } = useUser(); - // toast alert - const { setToastAlert } = useToast(); const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => { if (!workspaceSlug || !projectId || !moduleId) return; @@ -61,8 +60,8 @@ export const ModuleEmptyState: React.FC = observer((props) => { await issues .addIssuesToModule(workspaceSlug.toString(), projectId?.toString(), moduleId.toString(), issueIds) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Selected issues could not be added to the module. Please try again.", }) diff --git a/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx index 7ed6a8730..10b73ab35 100644 --- a/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx @@ -5,13 +5,14 @@ import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; // hooks import { useEventTracker, useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { createIssuePayload } from "helpers/issue.helper"; import { cn } from "helpers/common.helper"; +// ui +import { setPromiseToast } from "@plane/ui"; // types import { IProject, TIssue } from "@plane/types"; // constants @@ -70,7 +71,6 @@ export const GanttQuickAddIssueForm: React.FC = observe // hooks const { getProjectById } = useProject(); const { captureIssueEvent } = useEventTracker(); - const { setToastAlert } = useToast(); const projectDetail = (projectId && getProjectById(projectId.toString())) || undefined; @@ -110,31 +110,35 @@ export const GanttQuickAddIssueForm: React.FC = observe target_date: renderFormattedPayloadDate(targetDate), }); - try { - quickAddCallback && - (await quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId).then((res) => { + if (quickAddCallback) { + const quickAddPromise = quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId); + setPromiseToast(quickAddPromise, { + loading: "Adding issue...", + success: { + title: "Success!", + message: () => "Issue created successfully.", + }, + error: { + title: "Error!", + message: (err) => err?.message || "Some error occurred. Please try again.", + }, + }); + + await quickAddPromise + .then((res) => { captureIssueEvent({ eventName: ISSUE_CREATED, payload: { ...res, state: "SUCCESS", element: "Gantt quick add" }, path: router.asPath, }); - })); - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue created successfully.", - }); - } catch (err: any) { - captureIssueEvent({ - eventName: ISSUE_CREATED, - payload: { ...payload, state: "FAILED", element: "Gantt quick add" }, - path: router.asPath, - }); - setToastAlert({ - type: "error", - title: "Error!", - message: err?.message || "Some error occurred. Please try again.", - }); + }) + .catch(() => { + captureIssueEvent({ + eventName: ISSUE_CREATED, + payload: { ...payload, state: "FAILED", element: "Gantt quick add" }, + path: router.asPath, + }); + }); } }; return ( diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx index 7bdaf282d..3951e7032 100644 --- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -4,9 +4,8 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // hooks import { useEventTracker, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Spinner } from "@plane/ui"; +import { Spinner, TOAST_TYPE, setToast } from "@plane/ui"; // types import { TIssue } from "@plane/types"; import { EIssueActions } from "../types"; @@ -80,8 +79,6 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas } = useUser(); const { captureIssueEvent } = useEventTracker(); const { issueMap } = useIssues(); - // toast alert - const { setToastAlert } = useToast(); const issueIds = issues?.groupedIssueIds || []; @@ -159,9 +156,9 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas issueIds, viewId ).catch((err) => { - setToastAlert({ + setToast({ title: "Error", - type: "error", + type: TOAST_TYPE.ERROR, message: err.detail ?? "Failed to perform this action", }); }); diff --git a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx index f49af2922..d2f1febd7 100644 --- a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx @@ -1,13 +1,13 @@ import React, { FC } from "react"; import { useRouter } from "next/router"; +// ui +import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; // components -import { CustomMenu } from "@plane/ui"; import { ExistingIssuesListModal } from "components/core"; import { CreateUpdateIssueModal } from "components/issues"; // lucide icons import { Minimize2, Maximize2, Circle, Plus } from "lucide-react"; // hooks -import useToast from "hooks/use-toast"; import { useEventTracker } from "hooks/store"; // mobx import { observer } from "mobx-react-lite"; @@ -56,8 +56,6 @@ export const HeaderGroupByCard: FC = observer((props) => { const isDraftIssue = router.pathname.includes("draft-issue"); - const { setToastAlert } = useToast(); - const renderExistingIssueModal = moduleId || cycleId; const ExistingIssuesListModalPayload = moduleId ? { module: moduleId.toString() } : { cycle: true }; @@ -69,8 +67,8 @@ export const HeaderGroupByCard: FC = observer((props) => { try { addIssuesToView && addIssuesToView(issues); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Selected issues could not be added to the cycle. Please try again.", }); diff --git a/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx index 513163431..20f0cd8e0 100644 --- a/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx @@ -5,11 +5,12 @@ import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; // hooks import { useEventTracker, useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // helpers import { createIssuePayload } from "helpers/issue.helper"; +// ui +import { setPromiseToast } from "@plane/ui"; // types import { TIssue } from "@plane/types"; // constants @@ -73,7 +74,6 @@ export const KanBanQuickAddIssueForm: React.FC = obser useKeypress("Escape", handleClose); useOutsideClickDetector(ref, handleClose); - const { setToastAlert } = useToast(); const { reset, @@ -97,39 +97,42 @@ export const KanBanQuickAddIssueForm: React.FC = obser ...formData, }); - try { - quickAddCallback && - (await quickAddCallback( - workspaceSlug.toString(), - projectId.toString(), - { - ...payload, - }, - viewId - ).then((res) => { + if (quickAddCallback) { + const quickAddPromise = quickAddCallback( + workspaceSlug.toString(), + projectId.toString(), + { + ...payload, + }, + viewId + ); + setPromiseToast(quickAddPromise, { + loading: "Adding issue...", + success: { + title: "Success!", + message: () => "Issue created successfully.", + }, + error: { + title: "Error!", + message: (err) => err?.message || "Some error occurred. Please try again.", + }, + }); + + await quickAddPromise + .then((res) => { captureIssueEvent({ eventName: ISSUE_CREATED, payload: { ...res, state: "SUCCESS", element: "Kanban quick add" }, path: router.asPath, }); - })); - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue created successfully.", - }); - } catch (err: any) { - captureIssueEvent({ - eventName: ISSUE_CREATED, - payload: { ...payload, state: "FAILED", element: "Kanban quick add" }, - path: router.asPath, - }); - console.error(err); - setToastAlert({ - type: "error", - title: "Error!", - message: err?.message || "Some error occurred. Please try again.", - }); + }) + .catch(() => { + captureIssueEvent({ + eventName: ISSUE_CREATED, + payload: { ...payload, state: "FAILED", element: "Kanban quick add" }, + path: router.asPath, + }); + }); } }; 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 8d9164b37..404107af4 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 @@ -4,14 +4,14 @@ import { CircleDashed, Plus } from "lucide-react"; // components import { CreateUpdateIssueModal } from "components/issues"; import { ExistingIssuesListModal } from "components/core"; -import { CustomMenu } from "@plane/ui"; +// ui +import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; // mobx import { observer } from "mobx-react-lite"; // hooks import { useEventTracker } from "hooks/store"; // types import { TIssue, ISearchIssueResponse } from "@plane/types"; -import useToast from "hooks/use-toast"; import { useState } from "react"; import { TCreateModalStoreTypes } from "constants/issue"; @@ -38,8 +38,6 @@ export const HeaderGroupByCard = observer( const isDraftIssue = router.pathname.includes("draft-issue"); - const { setToastAlert } = useToast(); - const renderExistingIssueModal = moduleId || cycleId; const ExistingIssuesListModalPayload = moduleId ? { module: moduleId.toString() } : { cycle: true }; @@ -51,8 +49,8 @@ export const HeaderGroupByCard = observer( try { addIssuesToView && addIssuesToView(issues); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Selected issues could not be added to the cycle. Please try again.", }); diff --git a/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx index 8d1ce6d9c..3c71293b4 100644 --- a/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx @@ -5,12 +5,13 @@ import { PlusIcon } from "lucide-react"; import { observer } from "mobx-react-lite"; // hooks import { useEventTracker, useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; -// constants -import { TIssue, IProject } from "@plane/types"; +// ui +import { setPromiseToast } from "@plane/ui"; // types +import { TIssue, IProject } from "@plane/types"; +// helper import { createIssuePayload } from "helpers/issue.helper"; // constants import { ISSUE_CREATED } from "constants/event-tracker"; @@ -77,7 +78,6 @@ export const ListQuickAddIssueForm: FC = observer((props useKeypress("Escape", handleClose); useOutsideClickDetector(ref, handleClose); - const { setToastAlert } = useToast(); const { reset, @@ -101,31 +101,35 @@ export const ListQuickAddIssueForm: FC = observer((props ...formData, }); - try { - quickAddCallback && - (await quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId).then((res) => { + if (quickAddCallback) { + const quickAddPromise = quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId); + setPromiseToast(quickAddPromise, { + loading: "Adding issue...", + success: { + title: "Success!", + message: () => "Issue created successfully.", + }, + error: { + title: "Error!", + message: (err) => err?.message || "Some error occurred. Please try again.", + }, + }); + + await quickAddPromise + .then((res) => { captureIssueEvent({ eventName: ISSUE_CREATED, payload: { ...res, state: "SUCCESS", element: "List quick add" }, path: router.asPath, }); - })); - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue created successfully.", - }); - } catch (err: any) { - captureIssueEvent({ - eventName: ISSUE_CREATED, - payload: { ...payload, state: "FAILED", element: "List quick add" }, - path: router.asPath, - }); - setToastAlert({ - type: "error", - title: "Error!", - message: err?.message || "Some error occurred. Please try again.", - }); + }) + .catch(() => { + captureIssueEvent({ + eventName: ISSUE_CREATED, + payload: { ...payload, state: "FAILED", element: "List quick add" }, + path: router.asPath, + }); + }); } }; diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx index 20d21dc5f..c2ac29eef 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx @@ -1,12 +1,12 @@ import { useState } from "react"; import { useRouter } from "next/router"; -import { ArchiveIcon, CustomMenu } from "@plane/ui"; -import { observer } from "mobx-react"; import { Copy, ExternalLink, Link, Pencil, Trash2 } from "lucide-react"; import omit from "lodash/omit"; +import { observer } from "mobx-react"; // hooks -import useToast from "hooks/use-toast"; import { useEventTracker, useProjectState } from "hooks/store"; +// ui +import { ArchiveIcon, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; // components import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; // helpers @@ -39,8 +39,6 @@ export const AllIssueQuickActions: React.FC = observer((props // store hooks const { setTrackElement } = useEventTracker(); const { getStateById } = useProjectState(); - // toast alert - const { setToastAlert } = useToast(); // derived values const stateDetails = getStateById(issue.state_id); const isEditingAllowed = !readOnly; @@ -54,8 +52,8 @@ export const AllIssueQuickActions: React.FC = observer((props const handleOpenInNewTab = () => window.open(`/${issueLink}`, "_blank"); const handleCopyIssueLink = () => copyUrlToClipboard(issueLink).then(() => - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link copied", message: "Issue link copied to clipboard", }) diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx index 01e9b4921..a30db3a82 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx @@ -1,10 +1,10 @@ import { useState } from "react"; import { useRouter } from "next/router"; -import { CustomMenu } from "@plane/ui"; import { ExternalLink, Link, RotateCcw, Trash2 } from "lucide-react"; // hooks -import useToast from "hooks/use-toast"; import { useEventTracker, useIssues, useUser } from "hooks/store"; +// ui +import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; // components import { DeleteIssueModal } from "components/issues"; // helpers @@ -32,16 +32,14 @@ export const ArchivedIssueQuickActions: React.FC = (props) => // auth const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly; const isRestoringAllowed = handleRestore && isEditingAllowed; - // toast alert - const { setToastAlert } = useToast(); const issueLink = `${workspaceSlug}/projects/${issue.project_id}/archived-issues/${issue.id}`; const handleOpenInNewTab = () => window.open(`/${issueLink}`, "_blank"); const handleCopyIssueLink = () => copyUrlToClipboard(issueLink).then(() => - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link copied", message: "Issue link copied to clipboard", }) diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx index 8cd5dd56f..2b4a5fa05 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx @@ -1,12 +1,13 @@ import { useState } from "react"; import { useRouter } from "next/router"; -import { ArchiveIcon, CustomMenu } from "@plane/ui"; -import { observer } from "mobx-react"; -import { Copy, ExternalLink, Link, Pencil, Trash2, XCircle } from "lucide-react"; import omit from "lodash/omit"; +import { observer } from "mobx-react"; // hooks -import useToast from "hooks/use-toast"; import { useEventTracker, useIssues, useProjectState, useUser } from "hooks/store"; +// ui +import { ArchiveIcon, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; +// icons +import { Copy, ExternalLink, Link, Pencil, Trash2, XCircle } from "lucide-react"; // components import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; // helpers @@ -45,8 +46,6 @@ export const CycleIssueQuickActions: React.FC = observer((pro membership: { currentProjectRole }, } = useUser(); const { getStateById } = useProjectState(); - // toast alert - const { setToastAlert } = useToast(); // derived values const stateDetails = getStateById(issue.state_id); // auth @@ -64,8 +63,8 @@ export const CycleIssueQuickActions: React.FC = observer((pro const handleCopyIssueLink = () => copyUrlToClipboard(issueLink).then(() => - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link copied", message: "Issue link copied to clipboard", }) diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx index 13db0e9f1..cf090385d 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx @@ -1,12 +1,12 @@ import { useState } from "react"; import { useRouter } from "next/router"; -import { ArchiveIcon, CustomMenu } from "@plane/ui"; -import { observer } from "mobx-react"; -import { Copy, ExternalLink, Link, Pencil, Trash2, XCircle } from "lucide-react"; import omit from "lodash/omit"; +import { observer } from "mobx-react"; // hooks -import useToast from "hooks/use-toast"; import { useIssues, useEventTracker, useUser, useProjectState } from "hooks/store"; +// ui +import { ArchiveIcon, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; +import { Copy, ExternalLink, Link, Pencil, Trash2, XCircle } from "lucide-react"; // components import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; // helpers @@ -45,8 +45,6 @@ export const ModuleIssueQuickActions: React.FC = observer((pr membership: { currentProjectRole }, } = useUser(); const { getStateById } = useProjectState(); - // toast alert - const { setToastAlert } = useToast(); // derived values const stateDetails = getStateById(issue.state_id); // auth @@ -64,8 +62,8 @@ export const ModuleIssueQuickActions: React.FC = observer((pr const handleCopyIssueLink = () => copyUrlToClipboard(issueLink).then(() => - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link copied", message: "Issue link copied to clipboard", }) diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx index 99a8e6019..7afbd2421 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx @@ -1,12 +1,12 @@ import { useState } from "react"; import { useRouter } from "next/router"; -import { ArchiveIcon, CustomMenu } from "@plane/ui"; -import { observer } from "mobx-react"; -import { Copy, ExternalLink, Link, Pencil, Trash2 } from "lucide-react"; import omit from "lodash/omit"; +import { observer } from "mobx-react"; // hooks import { useEventTracker, useIssues, useProjectState, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { ArchiveIcon, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; +import { Copy, ExternalLink, Link, Pencil, Trash2 } from "lucide-react"; // components import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; // helpers @@ -54,16 +54,14 @@ export const ProjectIssueQuickActions: React.FC = observer((p !!stateDetails && [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group); const isDeletingAllowed = isEditingAllowed; - const { setToastAlert } = useToast(); - const issueLink = `${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`; const handleOpenInNewTab = () => window.open(`/${issueLink}`, "_blank"); const handleCopyIssueLink = () => copyUrlToClipboard(issueLink).then(() => - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link copied", message: "Issue link copied to clipboard", }) diff --git a/web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx index 3cba3c6cd..4b0d7aed4 100644 --- a/web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx @@ -5,11 +5,12 @@ import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; // hooks import { useEventTracker, useProject, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // helpers import { createIssuePayload } from "helpers/issue.helper"; +// ui +import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; // types import { TIssue } from "@plane/types"; // constants @@ -84,7 +85,6 @@ export const SpreadsheetQuickAddIssueForm: React.FC = observer((props) => // hooks useKeypress("Escape", handleClose); useOutsideClickDetector(ref, handleClose); - const { setToastAlert } = useToast(); useEffect(() => { setFocus("name"); @@ -100,13 +100,13 @@ export const SpreadsheetQuickAddIssueForm: React.FC = observer((props) => Object.keys(errors).forEach((key) => { const error = errors[key as keyof TIssue]; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error?.message?.toString() || "Some error occurred. Please try again.", }); }); - }, [errors, setToastAlert]); + }, [errors]); // const onSubmitHandler = async (formData: TIssue) => { // if (isSubmitting || !workspaceSlug || !projectId) return; @@ -130,8 +130,8 @@ export const SpreadsheetQuickAddIssueForm: React.FC = observer((props) => // payload // ); - // setToastAlert({ - // type: "success", + // setToast({ + // type: TOAST_TYPE.SUCCESS, // title: "Success!", // message: "Issue created successfully.", // }); @@ -140,8 +140,8 @@ export const SpreadsheetQuickAddIssueForm: React.FC = observer((props) => // const error = err?.[key]; // const errorTitle = error ? (Array.isArray(error) ? error.join(", ") : error) : null; - // setToastAlert({ - // type: "error", + // setToast({ + // type: TOAST_TYPE.ERROR, // title: "Error!", // message: errorTitle || "Some error occurred. Please try again.", // }); @@ -159,34 +159,41 @@ export const SpreadsheetQuickAddIssueForm: React.FC = observer((props) => ...formData, }); - try { - quickAddCallback && - (await quickAddCallback(currentWorkspace.slug, currentProjectDetails.id, { ...payload } as TIssue, viewId).then( - (res) => { - captureIssueEvent({ - eventName: ISSUE_CREATED, - payload: { ...res, state: "SUCCESS", element: "Spreadsheet quick add" }, - path: router.asPath, - }); - } - )); - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue created successfully.", - }); - } catch (err: any) { - captureIssueEvent({ - eventName: ISSUE_CREATED, - payload: { ...payload, state: "FAILED", element: "Spreadsheet quick add" }, - path: router.asPath, - }); - console.error(err); - setToastAlert({ - type: "error", - title: "Error!", - message: err?.message || "Some error occurred. Please try again.", + if (quickAddCallback) { + const quickAddPromise = quickAddCallback( + currentWorkspace.slug, + currentProjectDetails.id, + { ...payload } as TIssue, + viewId + ); + setPromiseToast(quickAddPromise, { + loading: "Adding issue...", + success: { + title: "Success!", + message: () => "Issue created successfully.", + }, + error: { + title: "Error!", + message: (err) => err?.message || "Some error occurred. Please try again.", + }, }); + + await quickAddPromise + .then((res) => { + captureIssueEvent({ + eventName: ISSUE_CREATED, + payload: { ...res, state: "SUCCESS", element: "Spreadsheet quick add" }, + path: router.asPath, + }); + }) + .catch((err) => { + captureIssueEvent({ + eventName: ISSUE_CREATED, + payload: { ...payload, state: "FAILED", element: "Spreadsheet quick add" }, + path: router.asPath, + }); + console.error(err); + }); } }; diff --git a/web/components/issues/issue-modal/draft-issue-layout.tsx b/web/components/issues/issue-modal/draft-issue-layout.tsx index 1f9935c98..b4dae211d 100644 --- a/web/components/issues/issue-modal/draft-issue-layout.tsx +++ b/web/components/issues/issue-modal/draft-issue-layout.tsx @@ -2,10 +2,11 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // hooks -import useToast from "hooks/use-toast"; import { useEventTracker } from "hooks/store"; // services import { IssueDraftService } from "services/issue"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { IssueFormRoot } from "components/issues/issue-modal/form"; import { ConfirmIssueDiscard } from "components/issues"; @@ -43,8 +44,6 @@ export const DraftIssueLayout: React.FC = observer((props) => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // toast alert - const { setToastAlert } = useToast(); // store hooks const { captureIssueEvent } = useEventTracker(); @@ -61,8 +60,8 @@ export const DraftIssueLayout: React.FC = observer((props) => { await issueDraftService .createDraftIssue(workspaceSlug.toString(), projectId.toString(), payload) .then((res) => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Draft Issue created successfully.", }); @@ -76,8 +75,8 @@ export const DraftIssueLayout: React.FC = observer((props) => { onClose(false); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Issue could not be created. Please try again.", }); diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx index e2e4e784e..7fcb6cffa 100644 --- a/web/components/issues/issue-modal/form.tsx +++ b/web/components/issues/issue-modal/form.tsx @@ -7,7 +7,6 @@ import { LayoutPanelTop, Sparkle, X } from "lucide-react"; import { RichTextEditorWithRef } from "@plane/rich-text-editor"; // hooks import { useApplication, useEstimate, useIssueDetail, useMention, useProject, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; // services import { AIService } from "services/ai.service"; import { FileService } from "services/file.service"; @@ -27,7 +26,7 @@ import { StateDropdown, } from "components/dropdowns"; // ui -import { Button, CustomMenu, Input, Loader, ToggleSwitch } from "@plane/ui"; +import { Button, CustomMenu, Input, Loader, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // types @@ -125,8 +124,6 @@ export const IssueFormRoot: FC = observer((props) => { const { issue: { getIssueById }, } = useIssueDetail(); - // toast alert - const { setToastAlert } = useToast(); // form info const { formState: { errors, isDirty, isSubmitting }, @@ -199,8 +196,8 @@ export const IssueFormRoot: FC = observer((props) => { }) .then((res) => { if (res.response === "") - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Issue title isn't informative enough to generate the description. Please try with a different title.", @@ -211,14 +208,14 @@ export const IssueFormRoot: FC = observer((props) => { const error = err?.data?.error; if (err.status === 429) - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error || "You have reached the maximum number of requests of 50 requests per month per user.", }); else - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error || "Some error occurred. Please try again.", }); diff --git a/web/components/issues/issue-modal/modal.tsx b/web/components/issues/issue-modal/modal.tsx index 1da02f0ac..3b97f9c4e 100644 --- a/web/components/issues/issue-modal/modal.tsx +++ b/web/components/issues/issue-modal/modal.tsx @@ -13,11 +13,12 @@ import { useWorkspace, useIssueDetail, } from "hooks/store"; -import useToast from "hooks/use-toast"; import useLocalStorage from "hooks/use-local-storage"; // components import { DraftIssueLayout } from "./draft-issue-layout"; import { IssueFormRoot } from "./form"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // types import type { TIssue } from "@plane/types"; // constants @@ -89,8 +90,6 @@ export const CreateUpdateIssueModal: React.FC = observer((prop }; // router const router = useRouter(); - // toast alert - const { setToastAlert } = useToast(); // local storage const { storedValue: localStorageDraftIssues, setValue: setLocalStorageDraftIssue } = useLocalStorage< Record> @@ -186,9 +185,8 @@ export const CreateUpdateIssueModal: React.FC = observer((prop await addIssueToCycle(response, payload.cycle_id); if (payload.module_ids && payload.module_ids.length > 0 && storeType !== EIssuesStoreType.MODULE) await addIssueToModule(response, payload.module_ids); - - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Issue created successfully.", }); @@ -200,8 +198,8 @@ export const CreateUpdateIssueModal: React.FC = observer((prop !createMore && handleClose(); return response; } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Issue could not be created. Please try again.", }); @@ -221,8 +219,8 @@ export const CreateUpdateIssueModal: React.FC = observer((prop ? await draftIssues.updateIssue(workspaceSlug, payload.project_id, data.id, payload) : await currentIssueStore.updateIssue(workspaceSlug, payload.project_id, data.id, payload, viewId); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Issue updated successfully.", }); @@ -233,8 +231,8 @@ export const CreateUpdateIssueModal: React.FC = observer((prop }); handleClose(); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Issue could not be created. Please try again.", }); diff --git a/web/components/issues/peek-overview/header.tsx b/web/components/issues/peek-overview/header.tsx index 8db7fd0ac..8d8ec00df 100644 --- a/web/components/issues/peek-overview/header.tsx +++ b/web/components/issues/peek-overview/header.tsx @@ -3,11 +3,18 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react"; import { MoveRight, MoveDiagonal, Link2, Trash2, RotateCcw } from "lucide-react"; // ui -import { ArchiveIcon, CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon, Tooltip } from "@plane/ui"; +import { + ArchiveIcon, + CenterPanelIcon, + CustomSelect, + FullScreenPanelIcon, + SidePanelIcon, + Tooltip, + TOAST_TYPE, + setToast, +} from "@plane/ui"; // helpers import { copyUrlToClipboard } from "helpers/string.helper"; -// hooks -import useToast from "hooks/use-toast"; // store hooks import { useIssueDetail, useProjectState, useUser } from "hooks/store"; // helpers @@ -74,8 +81,6 @@ export const IssuePeekOverviewHeader: FC = observer((pr issue: { getIssueById }, } = useIssueDetail(); const { getStateById } = useProjectState(); - // hooks - const { setToastAlert } = useToast(); // derived values const issueDetails = getIssueById(issueId); const stateDetails = issueDetails ? getStateById(issueDetails?.state_id) : undefined; @@ -87,8 +92,8 @@ export const IssuePeekOverviewHeader: FC = observer((pr e.stopPropagation(); e.preventDefault(); copyUrlToClipboard(issueLink).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Issue link copied to clipboard.", }); diff --git a/web/components/issues/peek-overview/root.tsx b/web/components/issues/peek-overview/root.tsx index 466bffee7..b28cc5de6 100644 --- a/web/components/issues/peek-overview/root.tsx +++ b/web/components/issues/peek-overview/root.tsx @@ -2,8 +2,9 @@ import { FC, useEffect, useState, useMemo } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // hooks -import useToast from "hooks/use-toast"; import { useEventTracker, useIssueDetail, useIssues, useUser } from "hooks/store"; +// ui +import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; // components import { IssueView } from "components/issues"; // types @@ -20,13 +21,7 @@ interface IIssuePeekOverview { export type TIssuePeekOperations = { fetch: (workspaceSlug: string, projectId: string, issueId: string) => Promise; - update: ( - workspaceSlug: string, - projectId: string, - issueId: string, - data: Partial, - showToast?: boolean - ) => Promise; + update: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise; archive: (workspaceSlug: string, projectId: string, issueId: string) => Promise; restore: (workspaceSlug: string, projectId: string, issueId: string) => Promise; @@ -49,8 +44,6 @@ export type TIssuePeekOperations = { export const IssuePeekOverview: FC = observer((props) => { const { is_archived = false, is_draft = false } = props; - // hooks - const { setToastAlert } = useToast(); // router const router = useRouter(); const { @@ -86,49 +79,38 @@ export const IssuePeekOverview: FC = observer((props) => { console.error("Error fetching the parent issue"); } }, - update: async ( - workspaceSlug: string, - projectId: string, - issueId: string, - data: Partial, - showToast: boolean = true - ) => { - try { - await updateIssue(workspaceSlug, projectId, issueId, data); - if (showToast) - setToastAlert({ - title: "Issue updated successfully", - type: "success", - message: "Issue updated successfully", + update: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => { + await updateIssue(workspaceSlug, projectId, issueId, data) + .then(() => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...data, issueId, state: "SUCCESS", element: "Issue peek-overview" }, + updates: { + changed_property: Object.keys(data).join(","), + change_details: Object.values(data).join(","), + }, + path: router.asPath, + }); + }) + .catch(() => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { state: "FAILED", element: "Issue peek-overview" }, + path: router.asPath, + }); + setToast({ + title: "Issue update failed", + type: TOAST_TYPE.ERROR, + message: "Issue update failed", }); - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...data, issueId, state: "SUCCESS", element: "Issue peek-overview" }, - updates: { - changed_property: Object.keys(data).join(","), - change_details: Object.values(data).join(","), - }, - path: router.asPath, }); - } catch (error) { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { state: "FAILED", element: "Issue peek-overview" }, - path: router.asPath, - }); - setToastAlert({ - title: "Issue update failed", - type: "error", - message: "Issue update failed", - }); - } }, remove: async (workspaceSlug: string, projectId: string, issueId: string) => { try { removeIssue(workspaceSlug, projectId, issueId); - setToastAlert({ + setToast({ title: "Issue deleted successfully", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Issue deleted successfully", }); captureIssueEvent({ @@ -137,9 +119,9 @@ export const IssuePeekOverview: FC = observer((props) => { path: router.asPath, }); } catch (error) { - setToastAlert({ + setToast({ title: "Issue delete failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Issue delete failed", }); captureIssueEvent({ @@ -152,8 +134,8 @@ export const IssuePeekOverview: FC = observer((props) => { archive: async (workspaceSlug: string, projectId: string, issueId: string) => { try { await archiveIssue(workspaceSlug, projectId, issueId); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Issue archived successfully.", }); @@ -163,8 +145,8 @@ export const IssuePeekOverview: FC = observer((props) => { path: router.asPath, }); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Issue could not be archived. Please try again.", }); @@ -178,8 +160,8 @@ export const IssuePeekOverview: FC = observer((props) => { restore: async (workspaceSlug: string, projectId: string, issueId: string) => { try { await restoreIssue(workspaceSlug, projectId, issueId); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Issue restored successfully.", }); @@ -189,8 +171,8 @@ export const IssuePeekOverview: FC = observer((props) => { path: router.asPath, }); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Issue could not be restored. Please try again.", }); @@ -203,12 +185,19 @@ export const IssuePeekOverview: FC = observer((props) => { }, addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => { try { - await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds); - setToastAlert({ - title: "Cycle added to issue successfully", - type: "success", - message: "Issue added to issue successfully", + const addToCyclePromise = addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds); + setPromiseToast(addToCyclePromise, { + loading: "Adding cycle to issue...", + success: { + title: "Success!", + message: () => "Cycle added to issue successfully", + }, + error: { + title: "Error!", + message: () => "Cycle add to issue failed", + }, }); + await addToCyclePromise; captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { ...issueIds, state: "SUCCESS", element: "Issue peek-overview" }, @@ -228,21 +217,23 @@ export const IssuePeekOverview: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ - title: "Cycle add to issue failed", - type: "error", - message: "Cycle add to issue failed", - }); } }, removeIssueFromCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => { try { - const response = await removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId); - setToastAlert({ - title: "Cycle removed from issue successfully", - type: "success", - message: "Cycle removed from issue successfully", + const removeFromCyclePromise = removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId); + setPromiseToast(removeFromCyclePromise, { + loading: "Removing cycle from issue...", + success: { + title: "Success!", + message: () => "Cycle removed from issue successfully", + }, + error: { + title: "Error!", + message: () => "Cycle remove from issue failed", + }, }); + const response = await removeFromCyclePromise; captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" }, @@ -253,11 +244,6 @@ export const IssuePeekOverview: FC = observer((props) => { path: router.asPath, }); } catch (error) { - setToastAlert({ - title: "Cycle remove from issue failed", - type: "error", - message: "Cycle remove from issue failed", - }); captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { state: "FAILED", element: "Issue peek-overview" }, @@ -271,12 +257,19 @@ export const IssuePeekOverview: FC = observer((props) => { }, addModulesToIssue: async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => { try { - const response = await addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds); - setToastAlert({ - title: "Module added to issue successfully", - type: "success", - message: "Module added to issue successfully", + const addToModulePromise = addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds); + setPromiseToast(addToModulePromise, { + loading: "Adding module to issue...", + success: { + title: "Success!", + message: () => "Module added to issue successfully", + }, + error: { + title: "Error!", + message: () => "Module add to issue failed", + }, }); + const response = await addToModulePromise; captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" }, @@ -296,21 +289,23 @@ export const IssuePeekOverview: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ - title: "Module add to issue failed", - type: "error", - message: "Module add to issue failed", - }); } }, removeIssueFromModule: async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => { try { - await removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId); - setToastAlert({ - title: "Module removed from issue successfully", - type: "success", - message: "Module removed from issue successfully", + const removeFromModulePromise = removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId); + setPromiseToast(removeFromModulePromise, { + loading: "Removing module from issue...", + success: { + title: "Success!", + message: () => "Module removed from issue successfully", + }, + error: { + title: "Error!", + message: () => "Module remove from issue failed", + }, }); + await removeFromModulePromise; captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" }, @@ -330,11 +325,6 @@ export const IssuePeekOverview: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ - title: "Module remove from issue failed", - type: "error", - message: "Module remove from issue failed", - }); } }, removeModulesFromIssue: async ( @@ -343,20 +333,19 @@ export const IssuePeekOverview: FC = observer((props) => { issueId: string, moduleIds: string[] ) => { - try { - await removeModulesFromIssue(workspaceSlug, projectId, issueId, moduleIds); - setToastAlert({ - title: "Module removed from issue successfully", - type: "success", - message: "Module removed from issue successfully", - }); - } catch (error) { - setToastAlert({ - title: "Module remove from issue failed", - type: "error", - message: "Module remove from issue failed", - }); - } + const removeModulesFromIssuePromise = removeModulesFromIssue(workspaceSlug, projectId, issueId, moduleIds); + setPromiseToast(removeModulesFromIssuePromise, { + loading: "Removing module from issue...", + success: { + title: "Success!", + message: () => "Module removed from issue successfully", + }, + error: { + title: "Error!", + message: () => "Module remove from issue failed", + }, + }); + await removeModulesFromIssuePromise; }, }), [ @@ -372,7 +361,6 @@ export const IssuePeekOverview: FC = observer((props) => { addModulesToIssue, removeIssueFromModule, removeModulesFromIssue, - setToastAlert, captureIssueEvent, router.asPath, ] diff --git a/web/components/issues/peek-overview/view.tsx b/web/components/issues/peek-overview/view.tsx index cb2100ce0..f94901c45 100644 --- a/web/components/issues/peek-overview/view.tsx +++ b/web/components/issues/peek-overview/view.tsx @@ -5,7 +5,6 @@ import { observer } from "mobx-react-lite"; // hooks import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useKeypress from "hooks/use-keypress"; -import useToast from "hooks/use-toast"; // store hooks import { useIssueDetail } from "hooks/store"; // components @@ -50,15 +49,13 @@ export const IssueView: FC = observer((props) => { issue: { getIssueById }, } = useIssueDetail(); const issue = getIssueById(issueId); - // hooks - const { alerts } = useToast(); // remove peek id const removeRoutePeekId = () => { setPeekIssue(undefined); }; useOutsideClickDetector(issuePeekOverviewRef, () => { - if (!isAnyModalOpen && (!alerts || alerts.length === 0)) { + if (!isAnyModalOpen) { removeRoutePeekId(); } }); diff --git a/web/components/issues/sub-issues/root.tsx b/web/components/issues/sub-issues/root.tsx index 5e406116c..da49200dd 100644 --- a/web/components/issues/sub-issues/root.tsx +++ b/web/components/issues/sub-issues/root.tsx @@ -4,14 +4,13 @@ import { observer } from "mobx-react-lite"; import { Plus, ChevronRight, ChevronDown, Loader } from "lucide-react"; // hooks import { useEventTracker, useIssueDetail } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { ExistingIssuesListModal } from "components/core"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; import { IssueList } from "./issues-list"; import { ProgressBar } from "./progressbar"; // ui -import { CustomMenu } from "@plane/ui"; +import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; // types @@ -46,8 +45,6 @@ export const SubIssuesRoot: FC = observer((props) => { const { workspaceSlug, projectId, parentIssueId, disabled = false } = props; // router const router = useRouter(); - // store hooks - const { setToastAlert } = useToast(); const { issue: { getIssueById }, subIssues: { subIssuesByIssueId, stateDistributionByIssueId, subIssueHelpersByIssueId, setSubIssueHelpers }, @@ -128,8 +125,8 @@ export const SubIssuesRoot: FC = observer((props) => { copyText: (text: string) => { const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; copyTextToClipboard(`${originURL}/${text}`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Issue link copied to clipboard.", }); @@ -139,8 +136,8 @@ export const SubIssuesRoot: FC = observer((props) => { try { await fetchSubIssues(workspaceSlug, projectId, parentIssueId); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error fetching sub-issues", message: "Error fetching sub-issues", }); @@ -149,14 +146,14 @@ export const SubIssuesRoot: FC = observer((props) => { addSubIssue: async (workspaceSlug: string, projectId: string, parentIssueId: string, issueIds: string[]) => { try { await createSubIssues(workspaceSlug, projectId, parentIssueId, issueIds); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Sub-issues added successfully", message: "Sub-issues added successfully", }); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error adding sub-issue", message: "Error adding sub-issue", }); @@ -183,8 +180,8 @@ export const SubIssuesRoot: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Sub-issue updated successfully", message: "Sub-issue updated successfully", }); @@ -199,8 +196,8 @@ export const SubIssuesRoot: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error updating sub-issue", message: "Error updating sub-issue", }); @@ -210,8 +207,8 @@ export const SubIssuesRoot: FC = observer((props) => { try { setSubIssueHelpers(parentIssueId, "issue_loader", issueId); await removeSubIssue(workspaceSlug, projectId, parentIssueId, issueId); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Sub-issue removed successfully", message: "Sub-issue removed successfully", }); @@ -235,8 +232,8 @@ export const SubIssuesRoot: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error removing sub-issue", message: "Error removing sub-issue", }); @@ -246,8 +243,8 @@ export const SubIssuesRoot: FC = observer((props) => { try { setSubIssueHelpers(parentIssueId, "issue_loader", issueId); await deleteSubIssue(workspaceSlug, projectId, parentIssueId, issueId); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Issue deleted successfully", message: "Issue deleted successfully", }); @@ -263,15 +260,15 @@ export const SubIssuesRoot: FC = observer((props) => { payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, path: router.asPath, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error deleting issue", message: "Error deleting issue", }); } }, }), - [fetchSubIssues, createSubIssues, updateSubIssue, removeSubIssue, deleteSubIssue, setToastAlert, setSubIssueHelpers] + [fetchSubIssues, createSubIssues, updateSubIssue, removeSubIssue, deleteSubIssue, setSubIssueHelpers] ); const issue = getIssueById(parentIssueId); diff --git a/web/components/issues/title-input.tsx b/web/components/issues/title-input.tsx index 4a4057a6a..2db4eb4b5 100644 --- a/web/components/issues/title-input.tsx +++ b/web/components/issues/title-input.tsx @@ -32,7 +32,7 @@ export const IssueTitleInput: FC = observer((props) => { useEffect(() => { const textarea = document.querySelector("#title-input"); if (debouncedValue && debouncedValue !== value) { - issueOperations.update(workspaceSlug, projectId, issueId, { name: debouncedValue }, false).finally(() => { + issueOperations.update(workspaceSlug, projectId, issueId, { name: debouncedValue }).finally(() => { setIsSubmitting("saved"); if (textarea && !textarea.matches(":focus")) { const trimmedTitle = debouncedValue.trim(); diff --git a/web/components/labels/create-label-modal.tsx b/web/components/labels/create-label-modal.tsx index e53e91147..b6a3f63e8 100644 --- a/web/components/labels/create-label-modal.tsx +++ b/web/components/labels/create-label-modal.tsx @@ -7,9 +7,8 @@ import { Dialog, Popover, Transition } from "@headlessui/react"; import { ChevronDown } from "lucide-react"; // hooks import { useLabel } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import type { IIssueLabel, IState } from "@plane/types"; // constants @@ -64,8 +63,6 @@ export const CreateLabelModal: React.FC = observer((props) => { reset(defaultValues); }; - const { setToastAlert } = useToast(); - const onSubmit = async (formData: IIssueLabel) => { if (!workspaceSlug) return; @@ -75,9 +72,9 @@ export const CreateLabelModal: React.FC = observer((props) => { if (onSuccess) onSuccess(res); }) .catch((error) => { - setToastAlert({ + setToast({ title: "Oops!", - type: "error", + type: TOAST_TYPE.ERROR, message: error?.error ?? "Error while adding the label", }); reset(formData); diff --git a/web/components/labels/create-update-label-inline.tsx b/web/components/labels/create-update-label-inline.tsx index 2d2be046d..d30d48a6a 100644 --- a/web/components/labels/create-update-label-inline.tsx +++ b/web/components/labels/create-update-label-inline.tsx @@ -6,9 +6,8 @@ import { Controller, SubmitHandler, useForm } from "react-hook-form"; import { Popover, Transition } from "@headlessui/react"; // hooks import { useLabel } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IIssueLabel } from "@plane/types"; // fetch-keys @@ -35,8 +34,6 @@ export const CreateUpdateLabelInline = observer( const { workspaceSlug, projectId } = router.query; // store hooks const { createLabel, updateLabel } = useLabel(); - // toast alert - const { setToastAlert } = useToast(); // form info const { handleSubmit, @@ -65,9 +62,9 @@ export const CreateUpdateLabelInline = observer( reset(defaultValues); }) .catch((error) => { - setToastAlert({ + setToast({ title: "Oops!", - type: "error", + type: TOAST_TYPE.ERROR, message: error?.error ?? "Error while adding the label", }); reset(formData); @@ -83,9 +80,9 @@ export const CreateUpdateLabelInline = observer( handleClose(); }) .catch((error) => { - setToastAlert({ + setToast({ title: "Oops!", - type: "error", + type: TOAST_TYPE.ERROR, message: error?.error ?? "Error while updating the label", }); reset(formData); diff --git a/web/components/labels/delete-label-modal.tsx b/web/components/labels/delete-label-modal.tsx index 64d15eb65..83b3e807d 100644 --- a/web/components/labels/delete-label-modal.tsx +++ b/web/components/labels/delete-label-modal.tsx @@ -6,10 +6,8 @@ import { observer } from "mobx-react-lite"; import { useLabel } from "hooks/store"; // icons import { AlertTriangle } from "lucide-react"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import type { IIssueLabel } from "@plane/types"; @@ -28,8 +26,6 @@ export const DeleteLabelModal: React.FC = observer((props) => { const { deleteLabel } = useLabel(); // states const [isDeleteLoading, setIsDeleteLoading] = useState(false); - // hooks - const { setToastAlert } = useToast(); const handleClose = () => { onClose(); @@ -49,8 +45,8 @@ export const DeleteLabelModal: React.FC = observer((props) => { setIsDeleteLoading(false); const error = err?.error || "Label could not be deleted. Please try again."; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error, }); diff --git a/web/components/modules/delete-module-modal.tsx b/web/components/modules/delete-module-modal.tsx index bf2e529b7..de9571179 100644 --- a/web/components/modules/delete-module-modal.tsx +++ b/web/components/modules/delete-module-modal.tsx @@ -4,9 +4,8 @@ import { Dialog, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; // hooks import { useEventTracker, useModule } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { AlertTriangle } from "lucide-react"; // types @@ -30,8 +29,6 @@ export const DeleteModuleModal: React.FC = observer((props) => { // store hooks const { captureModuleEvent } = useEventTracker(); const { deleteModule } = useModule(); - // toast alert - const { setToastAlert } = useToast(); const handleClose = () => { onClose(); @@ -47,8 +44,8 @@ export const DeleteModuleModal: React.FC = observer((props) => { .then(() => { if (moduleId || peekModule) router.push(`/${workspaceSlug}/projects/${data.project_id}/modules`); handleClose(); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Module deleted successfully.", }); @@ -58,8 +55,8 @@ export const DeleteModuleModal: React.FC = observer((props) => { }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Module could not be deleted. Please try again.", }); diff --git a/web/components/modules/modal.tsx b/web/components/modules/modal.tsx index 47f331396..00781affe 100644 --- a/web/components/modules/modal.tsx +++ b/web/components/modules/modal.tsx @@ -4,7 +4,8 @@ import { useForm } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; // hooks import { useEventTracker, useModule, useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { ModuleForm } from "components/modules"; // types @@ -36,8 +37,6 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { const { captureModuleEvent } = useEventTracker(); const { workspaceProjectIds } = useProject(); const { createModule, updateModuleDetails } = useModule(); - // toast alert - const { setToastAlert } = useToast(); const handleClose = () => { reset(defaultValues); @@ -55,8 +54,8 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { await createModule(workspaceSlug.toString(), selectedProjectId, payload) .then((res) => { handleClose(); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Module created successfully.", }); @@ -66,8 +65,8 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { }); }) .catch((err) => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err.detail ?? "Module could not be created. Please try again.", }); @@ -86,8 +85,8 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { .then((res) => { handleClose(); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Module updated successfully.", }); @@ -97,8 +96,8 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { }); }) .catch((err) => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err.detail ?? "Module could not be updated. Please try again.", }); diff --git a/web/components/modules/module-card-item.tsx b/web/components/modules/module-card-item.tsx index 4dec3df6e..52cc6097b 100644 --- a/web/components/modules/module-card-item.tsx +++ b/web/components/modules/module-card-item.tsx @@ -5,11 +5,10 @@ import { observer } from "mobx-react-lite"; import { Info, LinkIcon, Pencil, Star, Trash2 } from "lucide-react"; // hooks import { useEventTracker, useMember, useModule, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { CreateUpdateModuleModal, DeleteModuleModal } from "components/modules"; // ui -import { Avatar, AvatarGroup, CustomMenu, LayersIcon, Tooltip } from "@plane/ui"; +import { Avatar, AvatarGroup, CustomMenu, LayersIcon, Tooltip, TOAST_TYPE, setToast, setPromiseToast } from "@plane/ui"; // helpers import { copyUrlToClipboard } from "helpers/string.helper"; import { renderFormattedDate } from "helpers/date-time.helper"; @@ -30,8 +29,6 @@ export const ModuleCardItem: React.FC = observer((props) => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // toast alert - const { setToastAlert } = useToast(); // store hooks const { membership: { currentProjectRole }, @@ -48,21 +45,27 @@ export const ModuleCardItem: React.FC = observer((props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId) - .then(() => { + const addToFavoritePromise = addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).then( + () => { captureEvent(MODULE_FAVORITED, { module_id: moduleId, element: "Grid layout", state: "SUCCESS", }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the module to favorites. Please try again.", - }); - }); + } + ); + + setPromiseToast(addToFavoritePromise, { + loading: "Adding module to favorites...", + success: { + title: "Success!", + message: () => "Module added to favorites.", + }, + error: { + title: "Error!", + message: () => "Couldn't add the module to favorites. Please try again.", + }, + }); }; const handleRemoveFromFavorites = (e: React.MouseEvent) => { @@ -70,29 +73,37 @@ export const ModuleCardItem: React.FC = observer((props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), moduleId) - .then(() => { - captureEvent(MODULE_UNFAVORITED, { - module_id: moduleId, - element: "Grid layout", - state: "SUCCESS", - }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't remove the module from favorites. Please try again.", - }); + const removeFromFavoritePromise = removeModuleFromFavorites( + workspaceSlug.toString(), + projectId.toString(), + moduleId + ).then(() => { + captureEvent(MODULE_UNFAVORITED, { + module_id: moduleId, + element: "Grid layout", + state: "SUCCESS", }); + }); + + setPromiseToast(removeFromFavoritePromise, { + loading: "Removing module from favorites...", + success: { + title: "Success!", + message: () => "Module removed from favorites.", + }, + error: { + title: "Error!", + message: () => "Couldn't remove the module from favorites. Please try again.", + }, + }); }; const handleCopyText = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${moduleId}`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Module link copied to clipboard.", }); diff --git a/web/components/modules/module-list-item.tsx b/web/components/modules/module-list-item.tsx index 3d7468f24..72ed16adf 100644 --- a/web/components/modules/module-list-item.tsx +++ b/web/components/modules/module-list-item.tsx @@ -5,11 +5,19 @@ import { observer } from "mobx-react-lite"; import { Check, Info, LinkIcon, Pencil, Star, Trash2, User2 } from "lucide-react"; // hooks import { useModule, useUser, useEventTracker, useMember } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { CreateUpdateModuleModal, DeleteModuleModal } from "components/modules"; // ui -import { Avatar, AvatarGroup, CircularProgressIndicator, CustomMenu, Tooltip } from "@plane/ui"; +import { + Avatar, + AvatarGroup, + CircularProgressIndicator, + CustomMenu, + Tooltip, + TOAST_TYPE, + setToast, + setPromiseToast, +} from "@plane/ui"; // helpers import { copyUrlToClipboard } from "helpers/string.helper"; import { renderFormattedDate } from "helpers/date-time.helper"; @@ -30,8 +38,6 @@ export const ModuleListItem: React.FC = observer((props) => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // toast alert - const { setToastAlert } = useToast(); // store hooks const { membership: { currentProjectRole }, @@ -48,21 +54,27 @@ export const ModuleListItem: React.FC = observer((props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId) - .then(() => { + const addToFavoritePromise = addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).then( + () => { captureEvent(MODULE_FAVORITED, { module_id: moduleId, element: "Grid layout", state: "SUCCESS", }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the module to favorites. Please try again.", - }); - }); + } + ); + + setPromiseToast(addToFavoritePromise, { + loading: "Adding module to favorites...", + success: { + title: "Success!", + message: () => "Module added to favorites.", + }, + error: { + title: "Error!", + message: () => "Couldn't add the module to favorites. Please try again.", + }, + }); }; const handleRemoveFromFavorites = (e: React.MouseEvent) => { @@ -70,29 +82,37 @@ export const ModuleListItem: React.FC = observer((props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), moduleId) - .then(() => { - captureEvent(MODULE_UNFAVORITED, { - module_id: moduleId, - element: "Grid layout", - state: "SUCCESS", - }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't remove the module from favorites. Please try again.", - }); + const removeFromFavoritePromise = removeModuleFromFavorites( + workspaceSlug.toString(), + projectId.toString(), + moduleId + ).then(() => { + captureEvent(MODULE_UNFAVORITED, { + module_id: moduleId, + element: "Grid layout", + state: "SUCCESS", }); + }); + + setPromiseToast(removeFromFavoritePromise, { + loading: "Removing module from favorites...", + success: { + title: "Success!", + message: () => "Module removed from favorites.", + }, + error: { + title: "Error!", + message: () => "Couldn't remove the module from favorites. Please try again.", + }, + }); }; const handleCopyText = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${moduleId}`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Module link copied to clipboard.", }); diff --git a/web/components/modules/sidebar.tsx b/web/components/modules/sidebar.tsx index c8c55321c..ad3da373c 100644 --- a/web/components/modules/sidebar.tsx +++ b/web/components/modules/sidebar.tsx @@ -16,14 +16,22 @@ import { } from "lucide-react"; // hooks import { useModule, useUser, useEventTracker } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { LinkModal, LinksList, SidebarProgressStats } from "components/core"; import { DeleteModuleModal } from "components/modules"; import ProgressChart from "components/core/sidebar/progress-chart"; import { DateRangeDropdown, MemberDropdown } from "components/dropdowns"; // ui -import { CustomMenu, Loader, LayersIcon, CustomSelect, ModuleStatusIcon, UserGroupIcon } from "@plane/ui"; +import { + CustomMenu, + Loader, + LayersIcon, + CustomSelect, + ModuleStatusIcon, + UserGroupIcon, + TOAST_TYPE, + setToast, +} from "@plane/ui"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { copyUrlToClipboard } from "helpers/string.helper"; @@ -65,8 +73,6 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { const { setTrackElement, captureModuleEvent, captureEvent } = useEventTracker(); const moduleDetails = getModuleById(moduleId); - const { setToastAlert } = useToast(); - const { reset, control } = useForm({ defaultValues, }); @@ -99,15 +105,15 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { module_id: moduleId, state: "SUCCESS", }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Module link created", message: "Module link created successfully.", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Some error occurred", }); @@ -125,15 +131,15 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { module_id: moduleId, state: "SUCCESS", }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Module link updated", message: "Module link updated successfully.", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Some error occurred", }); @@ -149,15 +155,15 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { module_id: moduleId, state: "SUCCESS", }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Module link deleted", message: "Module link deleted successfully.", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Some error occurred", }); @@ -167,15 +173,15 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { const handleCopyText = () => { copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${moduleId}`) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link copied", message: "Module link copied to clipboard", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Some error occurred", }); @@ -187,8 +193,8 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { start_date: startDate ? renderFormattedPayloadDate(startDate) : null, target_date: targetDate ? renderFormattedPayloadDate(targetDate) : null, }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Module updated successfully.", }); diff --git a/web/components/notifications/notification-card.tsx b/web/components/notifications/notification-card.tsx index 03d849a82..bd26dcfa5 100644 --- a/web/components/notifications/notification-card.tsx +++ b/web/components/notifications/notification-card.tsx @@ -5,10 +5,9 @@ import Link from "next/link"; import { Menu } from "@headlessui/react"; import { ArchiveRestore, Clock, MessageSquare, MoreVertical, User2 } from "lucide-react"; // hooks -import useToast from "hooks/use-toast"; import { useEventTracker } from "hooks/store"; // icons -import { ArchiveIcon, CustomMenu, Tooltip } from "@plane/ui"; +import { ArchiveIcon, CustomMenu, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { snoozeOptions } from "constants/notification"; // helper @@ -50,8 +49,6 @@ export const NotificationCard: React.FC = (props) => { const { workspaceSlug } = router.query; // states const [showSnoozeOptions, setShowSnoozeOptions] = React.useState(false); - // toast alert - const { setToastAlert } = useToast(); // refs const snoozeRef = useRef(null); @@ -62,9 +59,9 @@ export const NotificationCard: React.FC = (props) => { icon: , onClick: () => { markNotificationReadStatusToggle(notification.id).then(() => { - setToastAlert({ + setToast({ title: notification.read_at ? "Notification marked as read" : "Notification marked as unread", - type: "success", + type: TOAST_TYPE.SUCCESS, }); }); }, @@ -79,9 +76,9 @@ export const NotificationCard: React.FC = (props) => { ), onClick: () => { markNotificationArchivedStatus(notification.id).then(() => { - setToastAlert({ + setToast({ title: notification.archived_at ? "Notification un-archived" : "Notification archived", - type: "success", + type: TOAST_TYPE.SUCCESS, }); }); }, @@ -94,9 +91,9 @@ export const NotificationCard: React.FC = (props) => { return; } markSnoozeNotification(notification.id, date).then(() => { - setToastAlert({ + setToast({ title: `Notification snoozed till ${renderFormattedDate(date)}`, - type: "success", + type: TOAST_TYPE.SUCCESS, }); }); }; @@ -330,9 +327,9 @@ export const NotificationCard: React.FC = (props) => { tab: selectedTab, state: "SUCCESS", }); - setToastAlert({ + setToast({ title: notification.read_at ? "Notification marked as read" : "Notification marked as unread", - type: "success", + type: TOAST_TYPE.SUCCESS, }); }); }, @@ -352,9 +349,9 @@ export const NotificationCard: React.FC = (props) => { tab: selectedTab, state: "SUCCESS", }); - setToastAlert({ + setToast({ title: notification.archived_at ? "Notification un-archived" : "Notification archived", - type: "success", + type: TOAST_TYPE.SUCCESS, }); }); }, @@ -403,9 +400,9 @@ export const NotificationCard: React.FC = (props) => { tab: selectedTab, state: "SUCCESS", }); - setToastAlert({ + setToast({ title: `Notification snoozed till ${renderFormattedDate(item.value)}`, - type: "success", + type: TOAST_TYPE.SUCCESS, }); }); }} diff --git a/web/components/notifications/select-snooze-till-modal.tsx b/web/components/notifications/select-snooze-till-modal.tsx index f89b3a963..c2875b8dd 100644 --- a/web/components/notifications/select-snooze-till-modal.tsx +++ b/web/components/notifications/select-snooze-till-modal.tsx @@ -6,10 +6,8 @@ import { Transition, Dialog } from "@headlessui/react"; import { X } from "lucide-react"; // constants import { allTimeIn30MinutesInterval12HoursFormat } from "constants/notification"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button, CustomSelect } from "@plane/ui"; +import { Button, CustomSelect, TOAST_TYPE, setToast } from "@plane/ui"; // types import type { IUserNotification } from "@plane/types"; @@ -41,8 +39,6 @@ export const SnoozeNotificationModal: FC = (props) => { const router = useRouter(); const { workspaceSlug } = router.query; - const { setToastAlert } = useToast(); - const { formState: { isSubmitting }, reset, @@ -100,10 +96,10 @@ export const SnoozeNotificationModal: FC = (props) => { await handleSubmitSnooze(notification.id, dateTime).then(() => { handleClose(); onSuccess(); - setToastAlert({ + setToast({ title: "Notification snoozed", message: "Notification snoozed successfully", - type: "success", + type: TOAST_TYPE.SUCCESS, }); }); }; diff --git a/web/components/onboarding/invite-members.tsx b/web/components/onboarding/invite-members.tsx index 561a428d6..1f78fcf20 100644 --- a/web/components/onboarding/invite-members.tsx +++ b/web/components/onboarding/invite-members.tsx @@ -17,10 +17,9 @@ import { Check, ChevronDown, Plus, XCircle } from "lucide-react"; // services import { WorkspaceService } from "services/workspace.service"; // hooks -import useToast from "hooks/use-toast"; import { useEventTracker } from "hooks/store"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // components import { OnboardingStepIndicator } from "components/onboarding/step-indicator"; // hooks @@ -269,7 +268,6 @@ export const InviteMembers: React.FC = (props) => { const [isInvitationDisabled, setIsInvitationDisabled] = useState(true); - const { setToastAlert } = useToast(); const { resolvedTheme } = useTheme(); // store hooks const { captureEvent } = useEventTracker(); @@ -322,8 +320,8 @@ export const InviteMembers: React.FC = (props) => { state: "SUCCESS", element: "Onboarding", }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Invitations sent successfully.", }); @@ -336,8 +334,8 @@ export const InviteMembers: React.FC = (props) => { state: "FAILED", element: "Onboarding", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error, }); diff --git a/web/components/onboarding/switch-delete-account-modal.tsx b/web/components/onboarding/switch-delete-account-modal.tsx index 66b98fb23..ff37e5802 100644 --- a/web/components/onboarding/switch-delete-account-modal.tsx +++ b/web/components/onboarding/switch-delete-account-modal.tsx @@ -6,7 +6,8 @@ import { Dialog, Transition } from "@headlessui/react"; import { Trash2 } from "lucide-react"; // hooks import { useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; type Props = { isOpen: boolean; @@ -25,8 +26,6 @@ export const SwitchOrDeleteAccountModal: React.FC = (props) => { const { resolvedTheme, setTheme } = useTheme(); - const { setToastAlert } = useToast(); - const handleClose = () => { setSwitchingAccount(false); setIsDeactivating(false); @@ -44,8 +43,8 @@ export const SwitchOrDeleteAccountModal: React.FC = (props) => { handleClose(); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Failed to sign out. Please try again.", }) @@ -58,8 +57,8 @@ export const SwitchOrDeleteAccountModal: React.FC = (props) => { await deactivateAccount() .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Account deleted successfully.", }); @@ -69,8 +68,8 @@ export const SwitchOrDeleteAccountModal: React.FC = (props) => { handleClose(); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error, }) diff --git a/web/components/onboarding/workspace.tsx b/web/components/onboarding/workspace.tsx index ad9342e3a..5ba5ca81c 100644 --- a/web/components/onboarding/workspace.tsx +++ b/web/components/onboarding/workspace.tsx @@ -1,12 +1,11 @@ import { useState } from "react"; import { Control, Controller, FieldErrors, UseFormHandleSubmit, UseFormSetValue } from "react-hook-form"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types"; // hooks import { useEventTracker, useUser, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; // services import { WorkspaceService } from "services/workspace.service"; // constants @@ -35,8 +34,6 @@ export const Workspace: React.FC = (props) => { const { updateCurrentUser } = useUser(); const { createWorkspace, fetchWorkspaces, workspaces } = useWorkspace(); const { captureWorkspaceEvent } = useEventTracker(); - // toast alert - const { setToastAlert } = useToast(); const handleCreateWorkspace = async (formData: IWorkspace) => { if (isSubmitting) return; @@ -49,8 +46,8 @@ export const Workspace: React.FC = (props) => { await createWorkspace(formData) .then(async (res) => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Workspace created successfully.", }); @@ -75,8 +72,8 @@ export const Workspace: React.FC = (props) => { element: "Onboarding", }, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Workspace could not be created. Please try again.", }); @@ -84,8 +81,8 @@ export const Workspace: React.FC = (props) => { } else setSlugError(true); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Some error occurred while creating workspace. Please try again.", }) diff --git a/web/components/pages/delete-page-modal.tsx b/web/components/pages/delete-page-modal.tsx index bba19b31c..67cd175f0 100644 --- a/web/components/pages/delete-page-modal.tsx +++ b/web/components/pages/delete-page-modal.tsx @@ -5,9 +5,8 @@ import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; // hooks import { useEventTracker, usePage } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import { useProjectPages } from "hooks/store/use-project-page"; // constants @@ -32,9 +31,6 @@ export const DeletePageModal: React.FC = observer((pr const { capturePageEvent } = useEventTracker(); const pageStore = usePage(pageId); - // toast alert - const { setToastAlert } = useToast(); - if (!pageStore) return null; const { name } = pageStore; @@ -60,8 +56,8 @@ export const DeletePageModal: React.FC = observer((pr }, }); handleClose(); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Page deleted successfully.", }); @@ -74,8 +70,8 @@ export const DeletePageModal: React.FC = observer((pr state: "FAILED", }, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Page could not be deleted. Please try again.", }); diff --git a/web/components/profile/preferences/email-notification-form.tsx b/web/components/profile/preferences/email-notification-form.tsx index e041b28d8..fd158e2d5 100644 --- a/web/components/profile/preferences/email-notification-form.tsx +++ b/web/components/profile/preferences/email-notification-form.tsx @@ -1,9 +1,7 @@ import React, { FC } from "react"; import { Controller, useForm } from "react-hook-form"; // ui -import { Button, Checkbox } from "@plane/ui"; -// hooks -import useToast from "hooks/use-toast"; +import { Button, Checkbox, TOAST_TYPE, setToast } from "@plane/ui"; // services import { UserService } from "services/user.service"; // types @@ -18,8 +16,6 @@ const userService = new UserService(); export const EmailNotificationForm: FC = (props) => { const { data } = props; - // toast - const { setToastAlert } = useToast(); // form data const { handleSubmit, @@ -45,9 +41,9 @@ export const EmailNotificationForm: FC = (props) => await userService .updateCurrentUserEmailNotificationSettings(payload) .then(() => - setToastAlert({ + setToast({ title: "Success", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Email Notification Settings updated successfully", }) ) diff --git a/web/components/project/card.tsx b/web/components/project/card.tsx index 3d6f62b7b..9f554cfea 100644 --- a/web/components/project/card.tsx +++ b/web/components/project/card.tsx @@ -5,11 +5,10 @@ import { LinkIcon, Lock, Pencil, Star } from "lucide-react"; import Link from "next/link"; // hooks import { useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { DeleteProjectModal, JoinProjectModal } from "components/project"; // ui -import { Avatar, AvatarGroup, Button, Tooltip } from "@plane/ui"; +import { Avatar, AvatarGroup, Button, Tooltip, TOAST_TYPE, setToast, setPromiseToast } from "@plane/ui"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; import { renderEmoji } from "helpers/emoji.helper"; @@ -27,8 +26,6 @@ export const ProjectCard: React.FC = observer((props) => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // toast alert - const { setToastAlert } = useToast(); // states const [deleteProjectModalOpen, setDeleteProjectModal] = useState(false); const [joinProjectModalOpen, setJoinProjectModal] = useState(false); @@ -42,24 +39,34 @@ export const ProjectCard: React.FC = observer((props) => { const handleAddToFavorites = () => { if (!workspaceSlug) return; - addProjectToFavorites(workspaceSlug.toString(), project.id).catch(() => { - setToastAlert({ - type: "error", + const addToFavoritePromise = addProjectToFavorites(workspaceSlug.toString(), project.id); + setPromiseToast(addToFavoritePromise, { + loading: "Adding project to favorites...", + success: { + title: "Success!", + message: () => "Project added to favorites.", + }, + error: { title: "Error!", - message: "Couldn't remove the project from favorites. Please try again.", - }); + message: () => "Couldn't add the project to favorites. Please try again.", + }, }); }; const handleRemoveFromFavorites = () => { if (!workspaceSlug || !project) return; - removeProjectFromFavorites(workspaceSlug.toString(), project.id).catch(() => { - setToastAlert({ - type: "error", + const removeFromFavoritePromise = removeProjectFromFavorites(workspaceSlug.toString(), project.id); + setPromiseToast(removeFromFavoritePromise, { + loading: "Removing project from favorites...", + success: { + title: "Success!", + message: () => "Project removed from favorites.", + }, + error: { title: "Error!", - message: "Couldn't remove the project from favorites. Please try again.", - }); + message: () => "Couldn't remove the project from favorites. Please try again.", + }, }); }; @@ -67,8 +74,8 @@ export const ProjectCard: React.FC = observer((props) => { const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${project.id}/issues`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Project link copied to clipboard.", }); diff --git a/web/components/project/create-project-modal.tsx b/web/components/project/create-project-modal.tsx index 49a42a0a3..f7bbd92cf 100644 --- a/web/components/project/create-project-modal.tsx +++ b/web/components/project/create-project-modal.tsx @@ -5,9 +5,8 @@ import { observer } from "mobx-react-lite"; import { X } from "lucide-react"; // hooks import { useEventTracker, useProject, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, CustomSelect, Input, TextArea } from "@plane/ui"; +import { Button, CustomSelect, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; // components import { ImagePickerPopover } from "components/core"; import EmojiIconPicker from "components/emoji-icon-picker"; @@ -32,16 +31,14 @@ interface IIsGuestCondition { } const IsGuestCondition: FC = ({ onClose }) => { - const { setToastAlert } = useToast(); - useEffect(() => { onClose(); - setToastAlert({ + setToast({ title: "Error", - type: "error", + type: TOAST_TYPE.ERROR, message: "You don't have permission to create project.", }); - }, [onClose, setToastAlert]); + }, [onClose]); return null; }; @@ -69,8 +66,6 @@ export const CreateProjectModal: FC = observer((props) => { const { addProjectToFavorites, createProject } = useProject(); // states const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true); - // toast - const { setToastAlert } = useToast(); // form info const cover_image = PROJECT_UNSPLASH_COVERS[Math.floor(Math.random() * PROJECT_UNSPLASH_COVERS.length)]; const { @@ -108,8 +103,8 @@ export const CreateProjectModal: FC = observer((props) => { if (!workspaceSlug) return; addProjectToFavorites(workspaceSlug.toString(), projectId).catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Couldn't remove the project from favorites. Please try again.", }); @@ -137,8 +132,8 @@ export const CreateProjectModal: FC = observer((props) => { eventName: PROJECT_CREATED, payload: newPayload, }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Project created successfully.", }); @@ -149,8 +144,8 @@ export const CreateProjectModal: FC = observer((props) => { }) .catch((err) => { Object.keys(err.data).map((key) => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err.data[key], }); diff --git a/web/components/project/delete-project-modal.tsx b/web/components/project/delete-project-modal.tsx index 791ac3672..844bd3aad 100644 --- a/web/components/project/delete-project-modal.tsx +++ b/web/components/project/delete-project-modal.tsx @@ -5,9 +5,8 @@ import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; // hooks import { useEventTracker, useProject, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import type { IProject } from "@plane/types"; // constants @@ -33,8 +32,6 @@ export const DeleteProjectModal: React.FC = (props) => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // toast alert - const { setToastAlert } = useToast(); // form info const { control, @@ -67,8 +64,8 @@ export const DeleteProjectModal: React.FC = (props) => { eventName: PROJECT_DELETED, payload: { ...project, state: "SUCCESS", element: "Project general settings" }, }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Project deleted successfully.", }); @@ -78,8 +75,8 @@ export const DeleteProjectModal: React.FC = (props) => { eventName: PROJECT_DELETED, payload: { ...project, state: "FAILED", element: "Project general settings" }, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong. Please try again later.", }); diff --git a/web/components/project/form.tsx b/web/components/project/form.tsx index 267103dc8..ef5a20024 100644 --- a/web/components/project/form.tsx +++ b/web/components/project/form.tsx @@ -2,11 +2,10 @@ import { FC, useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; // hooks import { useEventTracker, useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import EmojiIconPicker from "components/emoji-icon-picker"; import { ImagePickerPopover } from "components/core"; -import { Button, CustomSelect, Input, TextArea } from "@plane/ui"; +import { Button, CustomSelect, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { Lock } from "lucide-react"; // types @@ -33,8 +32,6 @@ export const ProjectDetailsForm: FC = (props) => { // store hooks const { captureProjectEvent } = useEventTracker(); const { updateProject } = useProject(); - // toast alert - const { setToastAlert } = useToast(); // form info const { handleSubmit, @@ -84,8 +81,8 @@ export const ProjectDetailsForm: FC = (props) => { element: "Project general settings", }, }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Project updated successfully", }); @@ -95,8 +92,8 @@ export const ProjectDetailsForm: FC = (props) => { eventName: PROJECT_UPDATED, payload: { ...payload, state: "FAILED", element: "Project general settings" }, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error?.error ?? "Project could not be updated. Please try again.", }); diff --git a/web/components/project/integration-card.tsx b/web/components/project/integration-card.tsx index d2910b34a..cf256098f 100644 --- a/web/components/project/integration-card.tsx +++ b/web/components/project/integration-card.tsx @@ -8,12 +8,13 @@ import useSWR, { mutate } from "swr"; import { ProjectService } from "services/project"; // hooks import { useRouter } from "next/router"; -import useToast from "hooks/use-toast"; // components import { SelectRepository, SelectChannel } from "components/integration"; // icons import GithubLogo from "public/logos/github-square.png"; import SlackLogo from "public/services/slack.png"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // types import { IWorkspaceIntegration } from "@plane/types"; // fetch-keys @@ -41,8 +42,6 @@ export const IntegrationCard: React.FC = ({ integration }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { setToastAlert } = useToast(); - const { data: syncedGithubRepository } = useSWR( projectId ? PROJECT_GITHUB_REPOSITORY(projectId as string) : null, () => @@ -71,16 +70,16 @@ export const IntegrationCard: React.FC = ({ integration }) => { .then(() => { mutate(PROJECT_GITHUB_REPOSITORY(projectId as string)); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: `${login}/${name} repository synced with the project successfully.`, }); }) .catch((err) => { console.error(err); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Repository could not be synced with the project. Please try again.", }); diff --git a/web/components/project/leave-project-modal.tsx b/web/components/project/leave-project-modal.tsx index 0827568ce..45618d4f2 100644 --- a/web/components/project/leave-project-modal.tsx +++ b/web/components/project/leave-project-modal.tsx @@ -6,9 +6,8 @@ import { AlertTriangleIcon } from "lucide-react"; import { observer } from "mobx-react-lite"; // hooks import { useEventTracker, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IProject } from "@plane/types"; // constants @@ -40,8 +39,6 @@ export const LeaveProjectModal: FC = observer((props) => { const { membership: { leaveProject }, } = useUser(); - // toast - const { setToastAlert } = useToast(); const { control, @@ -71,8 +68,8 @@ export const LeaveProjectModal: FC = observer((props) => { }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong please try again later.", }); @@ -82,22 +79,22 @@ export const LeaveProjectModal: FC = observer((props) => { }); }); } else { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please confirm leaving the project by typing the 'Leave Project'.", }); } } else { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please enter the project name as shown in the description.", }); } } else { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please fill all fields.", }); diff --git a/web/components/project/member-list-item.tsx b/web/components/project/member-list-item.tsx index 6a27eccd5..6bab775b8 100644 --- a/web/components/project/member-list-item.tsx +++ b/web/components/project/member-list-item.tsx @@ -4,11 +4,10 @@ import Link from "next/link"; import { observer } from "mobx-react-lite"; // hooks import { useEventTracker, useMember, useProject, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { ConfirmProjectMemberRemove } from "components/project"; // ui -import { CustomSelect, Tooltip } from "@plane/ui"; +import { CustomSelect, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { ChevronDown, Dot, XCircle } from "lucide-react"; // constants @@ -37,8 +36,6 @@ export const ProjectMemberListItem: React.FC = observer((props) => { project: { removeMemberFromProject, getProjectMemberDetails, updateMember }, } = useMember(); const { captureEvent } = useEventTracker(); - // toast alert - const { setToastAlert } = useToast(); // derived values const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN; @@ -58,8 +55,8 @@ export const ProjectMemberListItem: React.FC = observer((props) => { router.push(`/${workspaceSlug}/projects`); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error", message: err?.error || "Something went wrong. Please try again.", }) @@ -67,8 +64,8 @@ export const ProjectMemberListItem: React.FC = observer((props) => { } else await removeMemberFromProject(workspaceSlug.toString(), projectId.toString(), userDetails.member.id).catch( (err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error", message: err?.error || "Something went wrong. Please try again.", }) @@ -151,8 +148,8 @@ export const ProjectMemberListItem: React.FC = observer((props) => { const error = err.error; const errorString = Array.isArray(error) ? error[0] : error; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: errorString ?? "An error occurred while updating member role. Please try again.", }); diff --git a/web/components/project/project-settings-member-defaults.tsx b/web/components/project/project-settings-member-defaults.tsx index b4713f739..91c06cdfc 100644 --- a/web/components/project/project-settings-member-defaults.tsx +++ b/web/components/project/project-settings-member-defaults.tsx @@ -4,12 +4,11 @@ import useSWR from "swr"; import { observer } from "mobx-react-lite"; // hooks import { useProject, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; import { Controller, useForm } from "react-hook-form"; import { MemberSelect } from "components/project"; // ui -import { Loader } from "@plane/ui"; +import { Loader, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IProject, IUserLite, IWorkspace } from "@plane/types"; // fetch-keys @@ -33,8 +32,6 @@ export const ProjectSettingsMemberDefaults: React.FC = observer(() => { const { currentProjectDetails, fetchProjectDetails, updateProject } = useProject(); // derived values const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN; - // hooks - const { setToastAlert } = useToast(); // form info const { reset, control } = useForm({ defaultValues }); // fetching user members @@ -72,9 +69,9 @@ export const ProjectSettingsMemberDefaults: React.FC = observer(() => { }) .then(() => { fetchProjectDetails(workspaceSlug.toString(), projectId.toString()); - setToastAlert({ + setToast({ title: "Success", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Project updated successfully", }); }) diff --git a/web/components/project/publish-project/modal.tsx b/web/components/project/publish-project/modal.tsx index 77be4c84b..64cf87fb5 100644 --- a/web/components/project/publish-project/modal.tsx +++ b/web/components/project/publish-project/modal.tsx @@ -6,9 +6,8 @@ import { Dialog, Transition } from "@headlessui/react"; import { Check, CircleDot, Globe2 } from "lucide-react"; // hooks import { useProjectPublish } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, Loader, ToggleSwitch } from "@plane/ui"; +import { Button, Loader, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; import { CustomPopover } from "./popover"; // types import { IProject } from "@plane/types"; @@ -71,8 +70,6 @@ export const PublishProjectModal: React.FC = observer((props) => { unPublishProject, fetchSettingsLoader, } = useProjectPublish(); - // toast alert - const { setToastAlert } = useToast(); // form info const { control, @@ -150,8 +147,8 @@ export const PublishProjectModal: React.FC = observer((props) => { await updateProjectSettingsAsync(workspaceSlug.toString(), project.id, payload.id ?? "", payload) .then((res) => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Publish settings updated successfully!", }); @@ -176,8 +173,8 @@ export const PublishProjectModal: React.FC = observer((props) => { return res; }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong while un-publishing the project.", }) @@ -208,8 +205,8 @@ export const PublishProjectModal: React.FC = observer((props) => { const handleFormSubmit = async (formData: FormData) => { if (!formData.views || formData.views.length === 0) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please select at least one view layout to publish the project.", }); diff --git a/web/components/project/send-project-invitation-modal.tsx b/web/components/project/send-project-invitation-modal.tsx index ef7913fb0..da2f37e9f 100644 --- a/web/components/project/send-project-invitation-modal.tsx +++ b/web/components/project/send-project-invitation-modal.tsx @@ -6,9 +6,8 @@ import { Dialog, Transition } from "@headlessui/react"; import { ChevronDown, Plus, X } from "lucide-react"; // hooks import { useEventTracker, useMember, useUser, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Avatar, Button, CustomSelect, CustomSearchSelect } from "@plane/ui"; +import { Avatar, Button, CustomSelect, CustomSearchSelect, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { getUserRole } from "helpers/user.helper"; // constants @@ -45,8 +44,6 @@ export const SendProjectInvitationModal: React.FC = observer((props) => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // toast alert - const { setToastAlert } = useToast(); // store hooks const { captureEvent } = useEventTracker(); const { @@ -84,9 +81,9 @@ export const SendProjectInvitationModal: React.FC = observer((props) => { .then(() => { if (onSuccess) onSuccess(); onClose(); - setToastAlert({ + setToast({ title: "Success", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Members added successfully.", }); captureEvent(PROJECT_MEMBER_ADDED, { diff --git a/web/components/project/settings/features-list.tsx b/web/components/project/settings/features-list.tsx index 22e69827e..efbcc0857 100644 --- a/web/components/project/settings/features-list.tsx +++ b/web/components/project/settings/features-list.tsx @@ -2,10 +2,10 @@ import { FC } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { ContrastIcon, FileText, Inbox, Layers } from "lucide-react"; -import { DiceIcon, ToggleSwitch } from "@plane/ui"; // hooks import { useEventTracker, useProject, useUser, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { DiceIcon, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IProject } from "@plane/types"; // constants @@ -58,13 +58,11 @@ export const ProjectFeaturesList: FC = observer(() => { } = useUser(); const { currentProjectDetails, updateProject } = useProject(); const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN; - // toast alert - const { setToastAlert } = useToast(); const handleSubmit = async (formData: Partial) => { if (!workspaceSlug || !projectId || !currentProjectDetails) return; - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Project feature updated successfully.", }); diff --git a/web/components/project/sidebar-list-item.tsx b/web/components/project/sidebar-list-item.tsx index 695e0bce4..00dc858d0 100644 --- a/web/components/project/sidebar-list-item.tsx +++ b/web/components/project/sidebar-list-item.tsx @@ -21,13 +21,22 @@ import { // hooks import { useApplication, useEventTracker, useInbox, useProject } from "hooks/store"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; -import useToast from "hooks/use-toast"; // helpers import { cn } from "helpers/common.helper"; import { getNumberCount } from "helpers/string.helper"; import { renderEmoji } from "helpers/emoji.helper"; +// ui +import { + CustomMenu, + Tooltip, + ArchiveIcon, + PhotoFilterIcon, + DiceIcon, + ContrastIcon, + LayersIcon, + setPromiseToast, +} from "@plane/ui"; // components -import { CustomMenu, Tooltip, ArchiveIcon, PhotoFilterIcon, DiceIcon, ContrastIcon, LayersIcon } from "@plane/ui"; import { LeaveProjectModal, PublishProjectModal } from "components/project"; import { EUserProjectRoles } from "constants/project"; @@ -93,8 +102,6 @@ export const ProjectSidebarListItem: React.FC = observer((props) => { // router const router = useRouter(); const { workspaceSlug, projectId: URLProjectId } = router.query; - // toast alert - const { setToastAlert } = useToast(); // derived values const project = getProjectById(projectId); @@ -112,24 +119,34 @@ export const ProjectSidebarListItem: React.FC = observer((props) => { const handleAddToFavorites = () => { if (!workspaceSlug || !project) return; - addProjectToFavorites(workspaceSlug.toString(), project.id).catch(() => { - setToastAlert({ - type: "error", + const addToFavoritePromise = addProjectToFavorites(workspaceSlug.toString(), project.id); + setPromiseToast(addToFavoritePromise, { + loading: "Adding project to favorites...", + success: { + title: "Success!", + message: () => "Project added to favorites.", + }, + error: { title: "Error!", - message: "Couldn't remove the project from favorites. Please try again.", - }); + message: () => "Couldn't add the project to favorites. Please try again.", + }, }); }; const handleRemoveFromFavorites = () => { if (!workspaceSlug || !project) return; - removeProjectFromFavorites(workspaceSlug.toString(), project.id).catch(() => { - setToastAlert({ - type: "error", + const removeFromFavoritePromise = removeProjectFromFavorites(workspaceSlug.toString(), project.id); + setPromiseToast(removeFromFavoritePromise, { + loading: "Removing project from favorites...", + success: { + title: "Success!", + message: () => "Project removed from favorites.", + }, + error: { title: "Error!", - message: "Couldn't remove the project from favorites. Please try again.", - }); + message: () => "Couldn't remove the project from favorites. Please try again.", + }, }); }; diff --git a/web/components/project/sidebar-list.tsx b/web/components/project/sidebar-list.tsx index 983e23932..05e09f565 100644 --- a/web/components/project/sidebar-list.tsx +++ b/web/components/project/sidebar-list.tsx @@ -6,7 +6,8 @@ import { observer } from "mobx-react-lite"; import { ChevronDown, ChevronRight, Plus } from "lucide-react"; // hooks import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { CreateProjectModal, ProjectSidebarListItem } from "components/project"; // helpers @@ -42,15 +43,13 @@ export const ProjectSidebarList: FC = observer(() => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // toast - const { setToastAlert } = useToast(); const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; const handleCopyText = (projectId: string) => { copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/issues`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Project link copied to clipboard.", }); @@ -72,8 +71,8 @@ export const ProjectSidebarList: FC = observer(() => { const updatedSortOrder = orderJoinedProjects(source.index, destination.index, draggableId, joinedProjectsList); if (updatedSortOrder != undefined) updateProjectView(workspaceSlug.toString(), draggableId, { sort_order: updatedSortOrder }).catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong. Please try again.", }); diff --git a/web/components/states/create-state-modal.tsx b/web/components/states/create-state-modal.tsx index db91bb6b0..f39e3f335 100644 --- a/web/components/states/create-state-modal.tsx +++ b/web/components/states/create-state-modal.tsx @@ -6,9 +6,8 @@ import { Dialog, Popover, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; // hooks import { useProjectState } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, CustomSelect, Input, TextArea } from "@plane/ui"; +import { Button, CustomSelect, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { ChevronDown } from "lucide-react"; // types @@ -37,8 +36,6 @@ export const CreateStateModal: React.FC = observer((props) => { const { workspaceSlug } = router.query; // store hooks const { createState } = useProjectState(); - // toast alert - const { setToastAlert } = useToast(); // form info const { formState: { errors, isSubmitting }, @@ -71,15 +68,15 @@ export const CreateStateModal: React.FC = observer((props) => { if (typeof error === "object") { Object.keys(error).forEach((key) => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: Array.isArray(error[key]) ? error[key].join(", ") : error[key], }); }); } else { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error ?? err.status === 400 diff --git a/web/components/states/create-update-state-inline.tsx b/web/components/states/create-update-state-inline.tsx index 037cd483d..0a50208cd 100644 --- a/web/components/states/create-update-state-inline.tsx +++ b/web/components/states/create-update-state-inline.tsx @@ -6,9 +6,8 @@ import { Popover, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; // hooks import { useEventTracker, useProjectState } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, CustomSelect, Input, Tooltip } from "@plane/ui"; +import { Button, CustomSelect, Input, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // types import type { IState } from "@plane/types"; // constants @@ -39,8 +38,6 @@ export const CreateUpdateStateInline: React.FC = observer((props) => { // store hooks const { captureProjectStateEvent, setTrackElement } = useEventTracker(); const { createState, updateState } = useProjectState(); - // toast alert - const { setToastAlert } = useToast(); // form info const { handleSubmit, @@ -82,8 +79,8 @@ export const CreateUpdateStateInline: React.FC = observer((props) => { await createState(workspaceSlug.toString(), projectId.toString(), formData) .then((res) => { handleClose(); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "State created successfully.", }); @@ -98,14 +95,14 @@ export const CreateUpdateStateInline: React.FC = observer((props) => { }) .catch((error) => { if (error.status === 400) - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "State with that name already exists. Please try again with another name.", }); else - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "State could not be created. Please try again.", }); @@ -135,22 +132,22 @@ export const CreateUpdateStateInline: React.FC = observer((props) => { element: "Project settings states page", }, }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "State updated successfully.", }); }) .catch((error) => { if (error.status === 400) - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Another state exists with the same name. Please try again with another name.", }); else - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "State could not be updated. Please try again.", }); diff --git a/web/components/states/delete-state-modal.tsx b/web/components/states/delete-state-modal.tsx index 12de38608..df47c8b12 100644 --- a/web/components/states/delete-state-modal.tsx +++ b/web/components/states/delete-state-modal.tsx @@ -5,9 +5,8 @@ import { observer } from "mobx-react-lite"; import { AlertTriangle } from "lucide-react"; // hooks import { useEventTracker, useProjectState } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import type { IState } from "@plane/types"; // constants @@ -29,8 +28,6 @@ export const DeleteStateModal: React.FC = observer((props) => { // store hooks const { captureProjectStateEvent } = useEventTracker(); const { deleteState } = useProjectState(); - // toast alert - const { setToastAlert } = useToast(); const handleClose = () => { onClose(); @@ -55,15 +52,15 @@ export const DeleteStateModal: React.FC = observer((props) => { }) .catch((err) => { if (err.status === 400) - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "This state contains some issues within it, please move them to some other state to delete this state.", }); else - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "State could not be deleted. Please try again.", }); diff --git a/web/components/toast-alert/index.tsx b/web/components/toast-alert/index.tsx deleted file mode 100644 index b4df6ea05..000000000 --- a/web/components/toast-alert/index.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from "react"; -// hooks -import useToast from "hooks/use-toast"; -// icons -import { AlertTriangle, CheckCircle, Info, X, XCircle } from "lucide-react"; - -const ToastAlerts = () => { - const { alerts, removeAlert } = useToast(); - - if (!alerts) return null; - - return ( -
    - {alerts.map((alert) => ( -
    -
    - -
    -
    -
    -
    - {alert.type === "success" ? ( -
    -
    -

    {alert.title}

    - {alert.message &&

    {alert.message}

    } -
    -
    -
    -
    - ))} -
    - ); -}; - -export default ToastAlerts; diff --git a/web/components/views/delete-view-modal.tsx b/web/components/views/delete-view-modal.tsx index 5bd477352..f4fe8e120 100644 --- a/web/components/views/delete-view-modal.tsx +++ b/web/components/views/delete-view-modal.tsx @@ -5,9 +5,8 @@ import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; // hooks import { useProjectView } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IProjectView } from "@plane/types"; @@ -26,8 +25,6 @@ export const DeleteProjectViewModal: React.FC = observer((props) => { const { workspaceSlug, projectId } = router.query; // store hooks const { deleteView } = useProjectView(); - // toast alert - const { setToastAlert } = useToast(); const handleClose = () => { onClose(); @@ -43,15 +40,15 @@ export const DeleteProjectViewModal: React.FC = observer((props) => { .then(() => { handleClose(); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "View deleted successfully.", }); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "View could not be deleted. Please try again.", }) diff --git a/web/components/views/modal.tsx b/web/components/views/modal.tsx index 43cea7d5c..a1abef1a4 100644 --- a/web/components/views/modal.tsx +++ b/web/components/views/modal.tsx @@ -3,7 +3,8 @@ import { observer } from "mobx-react-lite"; import { Dialog, Transition } from "@headlessui/react"; // hooks import { useProjectView } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { ProjectViewForm } from "components/views"; // types @@ -22,8 +23,6 @@ export const CreateUpdateProjectViewModal: FC = observer((props) => { const { data, isOpen, onClose, preLoadedData, workspaceSlug, projectId } = props; // store hooks const { createView, updateView } = useProjectView(); - // toast alert - const { setToastAlert } = useToast(); const handleClose = () => { onClose(); @@ -33,15 +32,15 @@ export const CreateUpdateProjectViewModal: FC = observer((props) => { await createView(workspaceSlug, projectId, payload) .then(() => { handleClose(); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "View created successfully.", }); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong. Please try again.", }) @@ -52,8 +51,8 @@ export const CreateUpdateProjectViewModal: FC = observer((props) => { await updateView(workspaceSlug, projectId, data?.id as string, payload) .then(() => handleClose()) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err.detail ?? "Something went wrong. Please try again.", }) diff --git a/web/components/views/view-list-item.tsx b/web/components/views/view-list-item.tsx index 48cc12ada..7ff1ee92e 100644 --- a/web/components/views/view-list-item.tsx +++ b/web/components/views/view-list-item.tsx @@ -5,11 +5,10 @@ import { observer } from "mobx-react-lite"; import { LinkIcon, PencilIcon, StarIcon, TrashIcon } from "lucide-react"; // hooks import { useProjectView, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { CreateUpdateProjectViewModal, DeleteProjectViewModal } from "components/views"; // ui -import { CustomMenu } from "@plane/ui"; +import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { calculateTotalFilters } from "helpers/filter.helper"; import { copyUrlToClipboard } from "helpers/string.helper"; @@ -30,8 +29,6 @@ export const ProjectViewListItem: React.FC = observer((props) => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // toast alert - const { setToastAlert } = useToast(); // store hooks const { membership: { currentProjectRole }, @@ -54,8 +51,8 @@ export const ProjectViewListItem: React.FC = observer((props) => { e.stopPropagation(); e.preventDefault(); copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/views/${view.id}`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "View link copied to clipboard.", }); diff --git a/web/components/web-hooks/create-webhook-modal.tsx b/web/components/web-hooks/create-webhook-modal.tsx index f8301bf53..ecbd4ccd3 100644 --- a/web/components/web-hooks/create-webhook-modal.tsx +++ b/web/components/web-hooks/create-webhook-modal.tsx @@ -5,11 +5,12 @@ import { Dialog, Transition } from "@headlessui/react"; import { WebhookForm } from "./form"; import { GeneratedHookDetails } from "./generated-hook-details"; // hooks -import useToast from "hooks/use-toast"; // helpers import { csvDownload } from "helpers/download.helper"; // utils import { getCurrentHookAsCSV } from "./utils"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // types import { IWebhook, IWorkspace, TWebhookEventTypes } from "@plane/types"; @@ -34,8 +35,6 @@ export const CreateWebhookModal: React.FC = (props) => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // toast - const { setToastAlert } = useToast(); const handleCreateWebhook = async (formData: IWebhook, webhookEventType: TWebhookEventTypes) => { if (!workspaceSlug) return; @@ -65,8 +64,8 @@ export const CreateWebhookModal: React.FC = (props) => { await createWebhook(workspaceSlug.toString(), payload) .then(({ webHook, secretKey }) => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Webhook created successfully.", }); @@ -77,8 +76,8 @@ export const CreateWebhookModal: React.FC = (props) => { csvDownload(csvData, `webhook-secret-key-${Date.now()}`); }) .catch((error) => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error?.error ?? "Something went wrong. Please try again.", }); diff --git a/web/components/web-hooks/delete-webhook-modal.tsx b/web/components/web-hooks/delete-webhook-modal.tsx index 6cc30bb57..52c7a6595 100644 --- a/web/components/web-hooks/delete-webhook-modal.tsx +++ b/web/components/web-hooks/delete-webhook-modal.tsx @@ -4,9 +4,8 @@ import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; // hooks import { useWebhook } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; interface IDeleteWebhook { isOpen: boolean; @@ -19,8 +18,6 @@ export const DeleteWebhookModal: FC = (props) => { const [isDeleting, setIsDeleting] = useState(false); // router const router = useRouter(); - // toast - const { setToastAlert } = useToast(); // store hooks const { removeWebhook } = useWebhook(); @@ -37,16 +34,16 @@ export const DeleteWebhookModal: FC = (props) => { removeWebhook(workspaceSlug.toString(), webhookId.toString()) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Webhook deleted successfully.", }); router.replace(`/${workspaceSlug}/settings/webhooks/`); }) .catch((error) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error?.error ?? "Something went wrong. Please try again.", }) diff --git a/web/components/web-hooks/form/secret-key.tsx b/web/components/web-hooks/form/secret-key.tsx index 2d6a69fd6..7e9d9deda 100644 --- a/web/components/web-hooks/form/secret-key.tsx +++ b/web/components/web-hooks/form/secret-key.tsx @@ -1,16 +1,16 @@ import { useState, FC } from "react"; import { useRouter } from "next/router"; -import { Button, Tooltip } from "@plane/ui"; import { Copy, Eye, EyeOff, RefreshCw } from "lucide-react"; import { observer } from "mobx-react-lite"; // hooks import { useWebhook, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; import { csvDownload } from "helpers/download.helper"; // utils import { getCurrentHookAsCSV } from "../utils"; +// ui +import { Button, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IWebhook } from "@plane/types"; @@ -29,23 +29,21 @@ export const WebhookSecretKey: FC = observer((props) => { // store hooks const { currentWorkspace } = useWorkspace(); const { currentWebhook, regenerateSecretKey, webhookSecretKey } = useWebhook(); - // hooks - const { setToastAlert } = useToast(); const handleCopySecretKey = () => { if (!webhookSecretKey) return; copyTextToClipboard(webhookSecretKey) .then(() => - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Secret key copied to clipboard.", }) ) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Error occurred while copying secret key.", }) @@ -59,8 +57,8 @@ export const WebhookSecretKey: FC = observer((props) => { regenerateSecretKey(workspaceSlug.toString(), data.id) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "New key regenerated successfully.", }); @@ -71,8 +69,8 @@ export const WebhookSecretKey: FC = observer((props) => { } }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }) diff --git a/web/components/workspace/create-workspace-form.tsx b/web/components/workspace/create-workspace-form.tsx index b4f164469..e8e40cf85 100644 --- a/web/components/workspace/create-workspace-form.tsx +++ b/web/components/workspace/create-workspace-form.tsx @@ -6,9 +6,8 @@ import { Controller, useForm } from "react-hook-form"; import { WorkspaceService } from "services/workspace.service"; // hooks import { useEventTracker, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, CustomSelect, Input } from "@plane/ui"; +import { Button, CustomSelect, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IWorkspace } from "@plane/types"; // constants @@ -51,8 +50,6 @@ export const CreateWorkspaceForm: FC = observer((props) => { // store hooks const { captureWorkspaceEvent } = useEventTracker(); const { createWorkspace } = useWorkspace(); - // toast alert - const { setToastAlert } = useToast(); // form info const { handleSubmit, @@ -79,8 +76,8 @@ export const CreateWorkspaceForm: FC = observer((props) => { element: "Create workspace page", }, }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Workspace created successfully.", }); @@ -95,8 +92,8 @@ export const CreateWorkspaceForm: FC = observer((props) => { element: "Create workspace page", }, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Workspace could not be created. Please try again.", }); @@ -104,8 +101,8 @@ export const CreateWorkspaceForm: FC = observer((props) => { } else setSlugError(true); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Some error occurred while creating workspace. Please try again.", }); diff --git a/web/components/workspace/delete-workspace-modal.tsx b/web/components/workspace/delete-workspace-modal.tsx index a90ac9cdf..dbb2ef4f0 100644 --- a/web/components/workspace/delete-workspace-modal.tsx +++ b/web/components/workspace/delete-workspace-modal.tsx @@ -6,9 +6,8 @@ import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; // hooks import { useEventTracker, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import type { IWorkspace } from "@plane/types"; // constants @@ -32,8 +31,6 @@ export const DeleteWorkspaceModal: React.FC = observer((props) => { // store hooks const { captureWorkspaceEvent } = useEventTracker(); const { deleteWorkspace } = useWorkspace(); - // toast alert - const { setToastAlert } = useToast(); // form info const { control, @@ -69,15 +66,15 @@ export const DeleteWorkspaceModal: React.FC = observer((props) => { element: "Workspace general settings page", }, }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Workspace deleted successfully.", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong. Please try again later.", }); diff --git a/web/components/workspace/settings/invitations-list-item.tsx b/web/components/workspace/settings/invitations-list-item.tsx index 9e37a2fb5..9a9df5cb1 100644 --- a/web/components/workspace/settings/invitations-list-item.tsx +++ b/web/components/workspace/settings/invitations-list-item.tsx @@ -4,11 +4,10 @@ import { observer } from "mobx-react-lite"; import { ChevronDown, XCircle } from "lucide-react"; // hooks import { useMember, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { ConfirmWorkspaceMemberRemove } from "components/workspace"; // ui -import { CustomSelect, Tooltip } from "@plane/ui"; +import { CustomSelect, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { EUserWorkspaceRoles, ROLE } from "constants/workspace"; @@ -30,8 +29,6 @@ export const WorkspaceInvitationsListItem: FC = observer((props) => { const { workspace: { updateMemberInvitation, deleteMemberInvitation, getWorkspaceInvitationDetails }, } = useMember(); - // toast alert - const { setToastAlert } = useToast(); // derived values const invitationDetails = getWorkspaceInvitationDetails(invitationId); @@ -40,15 +37,15 @@ export const WorkspaceInvitationsListItem: FC = observer((props) => { await deleteMemberInvitation(workspaceSlug.toString(), invitationDetails.id) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success", message: "Invitation removed successfully.", }); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error", message: err?.error || "Something went wrong. Please try again.", }) @@ -116,8 +113,8 @@ export const WorkspaceInvitationsListItem: FC = observer((props) => { updateMemberInvitation(workspaceSlug.toString(), invitationDetails.id, { role: value, }).catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "An error occurred while updating member role. Please try again.", }); diff --git a/web/components/workspace/settings/members-list-item.tsx b/web/components/workspace/settings/members-list-item.tsx index 76c9bbedf..c6c8d1d36 100644 --- a/web/components/workspace/settings/members-list-item.tsx +++ b/web/components/workspace/settings/members-list-item.tsx @@ -5,11 +5,10 @@ import { observer } from "mobx-react-lite"; import { ChevronDown, Dot, XCircle } from "lucide-react"; // hooks import { useEventTracker, useMember, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { ConfirmWorkspaceMemberRemove } from "components/workspace"; // ui -import { CustomSelect, Tooltip } from "@plane/ui"; +import { CustomSelect, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { EUserWorkspaceRoles, ROLE } from "constants/workspace"; import { WORKSPACE_MEMBER_lEAVE } from "constants/event-tracker"; @@ -35,8 +34,6 @@ export const WorkspaceMembersListItem: FC = observer((props) => { workspace: { updateMember, removeMemberFromWorkspace, getWorkspaceMemberDetails }, } = useMember(); const { captureEvent } = useEventTracker(); - // toast alert - const { setToastAlert } = useToast(); // derived values const memberDetails = getWorkspaceMemberDetails(memberId); @@ -52,8 +49,8 @@ export const WorkspaceMembersListItem: FC = observer((props) => { router.push("/profile"); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error", message: err?.error || "Something went wrong. Please try again.", }) @@ -64,8 +61,8 @@ export const WorkspaceMembersListItem: FC = observer((props) => { if (!workspaceSlug || !memberDetails) return; await removeMemberFromWorkspace(workspaceSlug.toString(), memberDetails.member.id).catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error", message: err?.error || "Something went wrong. Please try again.", }) @@ -165,8 +162,8 @@ export const WorkspaceMembersListItem: FC = observer((props) => { updateMember(workspaceSlug.toString(), memberDetails.member.id, { role: value, }).catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "An error occurred while updating member role. Please try again.", }); diff --git a/web/components/workspace/settings/workspace-details.tsx b/web/components/workspace/settings/workspace-details.tsx index 44da4291f..d491ca08e 100644 --- a/web/components/workspace/settings/workspace-details.tsx +++ b/web/components/workspace/settings/workspace-details.tsx @@ -7,12 +7,11 @@ import { ChevronDown, ChevronUp, Pencil } from "lucide-react"; import { FileService } from "services/file.service"; // hooks import { useEventTracker, useUser, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { DeleteWorkspaceModal } from "components/workspace"; import { WorkspaceImageUploadModal } from "components/core"; // ui -import { Button, CustomSelect, Input, Spinner } from "@plane/ui"; +import { Button, CustomSelect, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { copyUrlToClipboard } from "helpers/string.helper"; // types @@ -43,8 +42,6 @@ export const WorkspaceDetails: FC = observer(() => { membership: { currentWorkspaceRole }, } = useUser(); const { currentWorkspace, updateWorkspace } = useWorkspace(); - // toast alert - const { setToastAlert } = useToast(); // form info const { handleSubmit, @@ -77,9 +74,9 @@ export const WorkspaceDetails: FC = observer(() => { element: "Workspace general settings page", }, }); - setToastAlert({ + setToast({ title: "Success", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Workspace updated successfully", }); }) @@ -110,16 +107,16 @@ export const WorkspaceDetails: FC = observer(() => { fileService.deleteFile(currentWorkspace.id, url).then(() => { updateWorkspace(currentWorkspace.slug, { logo: "" }) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Workspace picture removed successfully.", }); setIsImageUploadModalOpen(false); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "There was some error in deleting your profile picture. Please try again.", }); @@ -132,8 +129,8 @@ export const WorkspaceDetails: FC = observer(() => { if (!currentWorkspace) return; copyUrlToClipboard(`${currentWorkspace.slug}`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Workspace URL copied to the clipboard.", }); }); diff --git a/web/components/workspace/sidebar-dropdown.tsx b/web/components/workspace/sidebar-dropdown.tsx index 984bc1caf..98a133ee3 100644 --- a/web/components/workspace/sidebar-dropdown.tsx +++ b/web/components/workspace/sidebar-dropdown.tsx @@ -9,10 +9,8 @@ import { Check, ChevronDown, CircleUserRound, LogOut, Mails, PlusSquare, Setting import { usePopper } from "react-popper"; // hooks import { useApplication, useUser, useWorkspace } from "hooks/store"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Avatar, Loader } from "@plane/ui"; +import { Avatar, Loader, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IWorkspace } from "@plane/types"; // Static Data @@ -58,8 +56,6 @@ export const WorkspaceSidebarDropdown = observer(() => { } = useApplication(); const { currentUser, updateCurrentUser, isUserInstanceAdmin, signOut } = useUser(); const { currentWorkspace: activeWorkspace, workspaces } = useWorkspace(); - // hooks - const { setToastAlert } = useToast(); const { setTheme } = useTheme(); // popper-js refs const [referenceElement, setReferenceElement] = useState(null); @@ -88,8 +84,8 @@ export const WorkspaceSidebarDropdown = observer(() => { router.push("/"); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Failed to sign out. Please try again.", }) diff --git a/web/components/workspace/views/delete-view-modal.tsx b/web/components/workspace/views/delete-view-modal.tsx index b6028d541..85d56cc63 100644 --- a/web/components/workspace/views/delete-view-modal.tsx +++ b/web/components/workspace/views/delete-view-modal.tsx @@ -5,9 +5,8 @@ import { observer } from "mobx-react-lite"; import { AlertTriangle } from "lucide-react"; // store hooks import { useGlobalView, useEventTracker } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IWorkspaceView } from "@plane/types"; // constants @@ -29,8 +28,6 @@ export const DeleteGlobalViewModal: React.FC = observer((props) => { // store hooks const { deleteGlobalView } = useGlobalView(); const { captureEvent } = useEventTracker(); - // toast alert - const { setToastAlert } = useToast(); const handleClose = () => { onClose(); @@ -53,8 +50,8 @@ export const DeleteGlobalViewModal: React.FC = observer((props) => { view_id: data.id, state: "FAILED", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong while deleting the view. Please try again.", }); diff --git a/web/components/workspace/views/modal.tsx b/web/components/workspace/views/modal.tsx index b66d555fa..6543a8321 100644 --- a/web/components/workspace/views/modal.tsx +++ b/web/components/workspace/views/modal.tsx @@ -4,7 +4,8 @@ import { observer } from "mobx-react-lite"; import { Dialog, Transition } from "@headlessui/react"; // store hooks import { useEventTracker, useGlobalView } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { WorkspaceViewForm } from "components/workspace"; // types @@ -27,8 +28,6 @@ export const CreateUpdateWorkspaceViewModal: React.FC = observer((props) // store hooks const { createGlobalView, updateGlobalView } = useGlobalView(); const { captureEvent } = useEventTracker(); - // toast alert - const { setToastAlert } = useToast(); const handleClose = () => { onClose(); @@ -51,8 +50,8 @@ export const CreateUpdateWorkspaceViewModal: React.FC = observer((props) applied_filters: res.filters, state: "SUCCESS", }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "View created successfully.", }); @@ -65,8 +64,8 @@ export const CreateUpdateWorkspaceViewModal: React.FC = observer((props) applied_filters: payload?.filters, state: "FAILED", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "View could not be created. Please try again.", }); @@ -90,8 +89,8 @@ export const CreateUpdateWorkspaceViewModal: React.FC = observer((props) applied_filters: res.filters, state: "SUCCESS", }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "View updated successfully.", }); @@ -103,8 +102,8 @@ export const CreateUpdateWorkspaceViewModal: React.FC = observer((props) applied_filters: data.filters, state: "FAILED", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "View could not be updated. Please try again.", }); diff --git a/web/contexts/toast.context.tsx b/web/contexts/toast.context.tsx deleted file mode 100644 index 30e100b20..000000000 --- a/web/contexts/toast.context.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React, { createContext, useCallback, useReducer } from "react"; -// uuid -import { v4 as uuid } from "uuid"; -// components -import ToastAlert from "components/toast-alert"; - -export const toastContext = createContext({} as ContextType); - -// types -type ToastAlert = { - id: string; - title: string; - message?: string; - type: "success" | "error" | "warning" | "info"; -}; - -type ReducerActionType = { - type: "SET_TOAST_ALERT" | "REMOVE_TOAST_ALERT"; - payload: ToastAlert; -}; - -type ContextType = { - alerts?: ToastAlert[]; - removeAlert: (id: string) => void; - setToastAlert: (data: { - title: string; - type?: "success" | "error" | "warning" | "info" | undefined; - message?: string | undefined; - }) => void; -}; - -type StateType = { - toastAlerts?: ToastAlert[]; -}; - -type ReducerFunctionType = (state: StateType, action: ReducerActionType) => StateType; - -export const initialState: StateType = { - toastAlerts: [], -}; - -export const reducer: ReducerFunctionType = (state, action) => { - const { type, payload } = action; - - switch (type) { - case "SET_TOAST_ALERT": - return { - ...state, - toastAlerts: [...(state.toastAlerts ?? []), payload], - }; - - case "REMOVE_TOAST_ALERT": - return { - ...state, - toastAlerts: state.toastAlerts?.filter((toastAlert) => toastAlert.id !== payload.id), - }; - - default: { - return state; - } - } -}; - -export const ToastContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const [state, dispatch] = useReducer(reducer, initialState); - - const removeAlert = useCallback((id: string) => { - dispatch({ - type: "REMOVE_TOAST_ALERT", - payload: { id, title: "", message: "", type: "success" }, - }); - }, []); - - const setToastAlert = useCallback( - (data: { title: string; type?: "success" | "error" | "warning" | "info"; message?: string }) => { - const id = uuid(); - const { title, type, message } = data; - dispatch({ - type: "SET_TOAST_ALERT", - payload: { id, title, message, type: type ?? "success" }, - }); - - const timer = setTimeout(() => { - removeAlert(id); - clearTimeout(timer); - }, 3000); - }, - [removeAlert] - ); - - return ( - - - {children} - - ); -}; diff --git a/web/helpers/theme.helper.ts b/web/helpers/theme.helper.ts index 16cd8cd79..a9aa5b913 100644 --- a/web/helpers/theme.helper.ts +++ b/web/helpers/theme.helper.ts @@ -118,3 +118,6 @@ export const unsetCustomCssVariables = () => { dom?.style.removeProperty("--color-scheme"); } }; + +export const resolveGeneralTheme = (resolvedTheme: string | undefined) => + resolvedTheme?.includes("light") ? "light" : resolvedTheme?.includes("dark") ? "dark" : "system"; diff --git a/web/hooks/use-toast.tsx b/web/hooks/use-toast.tsx deleted file mode 100644 index 6de3c104c..000000000 --- a/web/hooks/use-toast.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { useContext } from "react"; -import { toastContext } from "contexts/toast.context"; - -const useToast = () => { - const toastContextData = useContext(toastContext); - return toastContextData; -}; - -export default useToast; diff --git a/web/hooks/use-user-notifications.tsx b/web/hooks/use-user-notifications.tsx index 17a2c63dc..3c2ec6332 100644 --- a/web/hooks/use-user-notifications.tsx +++ b/web/hooks/use-user-notifications.tsx @@ -5,12 +5,12 @@ import useSWR from "swr"; import useSWRInfinite from "swr/infinite"; // services import { NotificationService } from "services/notification.service"; -// hooks -import useToast from "./use-toast"; // fetch-keys import { UNREAD_NOTIFICATIONS_COUNT, getPaginatedNotificationKey } from "constants/fetch-keys"; // type import type { NotificationType, NotificationCount, IMarkAllAsReadPayload } from "@plane/types"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; const PER_PAGE = 30; @@ -20,8 +20,6 @@ const useUserNotification = () => { const router = useRouter(); const { workspaceSlug } = router.query; - const { setToastAlert } = useToast(); - const [snoozed, setSnoozed] = useState(false); const [archived, setArchived] = useState(false); const [readNotification, setReadNotification] = useState(false); @@ -265,15 +263,15 @@ const useUserNotification = () => { await userNotificationServices .markAllNotificationsAsRead(workspaceSlug.toString(), markAsReadParams) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "All Notifications marked as read.", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong. Please try again.", }); diff --git a/web/layouts/settings-layout/profile/sidebar.tsx b/web/layouts/settings-layout/profile/sidebar.tsx index 3e515cc64..4d78195f1 100644 --- a/web/layouts/settings-layout/profile/sidebar.tsx +++ b/web/layouts/settings-layout/profile/sidebar.tsx @@ -7,9 +7,8 @@ import { useTheme } from "next-themes"; import { ChevronLeft, LogOut, MoveLeft, Plus, UserPlus } from "lucide-react"; // hooks import { useApplication, useUser, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Tooltip } from "@plane/ui"; +import { Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { PROFILE_ACTION_LINKS } from "constants/profile"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; @@ -36,8 +35,6 @@ export const ProfileLayoutSidebar = observer(() => { const router = useRouter(); // next themes const { setTheme } = useTheme(); - // toast - const { setToastAlert } = useToast(); // store hooks const { theme: { sidebarCollapsed, toggleSidebar }, @@ -92,8 +89,8 @@ export const ProfileLayoutSidebar = observer(() => { router.push("/"); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Failed to sign out. Please try again.", }) diff --git a/web/lib/app-provider.tsx b/web/lib/app-provider.tsx index 9c06947af..a91793613 100644 --- a/web/lib/app-provider.tsx +++ b/web/lib/app-provider.tsx @@ -3,18 +3,19 @@ import dynamic from "next/dynamic"; import Router from "next/router"; import NProgress from "nprogress"; import { observer } from "mobx-react-lite"; -import { ThemeProvider } from "next-themes"; +import { useTheme } from "next-themes"; // hooks import { useApplication, useUser, useWorkspace } from "hooks/store"; +// ui +import { Toast } from "@plane/ui"; // constants -import { THEMES } from "constants/themes"; +import { SWR_CONFIG } from "constants/swr-config"; // layouts import InstanceLayout from "layouts/instance-layout"; // contexts -import { ToastContextProvider } from "contexts/toast.context"; import { SWRConfig } from "swr"; -// constants -import { SWR_CONFIG } from "constants/swr-config"; +//helpers +import { resolveGeneralTheme } from "helpers/theme.helper"; // dynamic imports const StoreWrapper = dynamic(() => import("lib/wrappers/store-wrapper"), { ssr: false }); const PostHogProvider = dynamic(() => import("lib/posthog-provider"), { ssr: false }); @@ -41,27 +42,29 @@ export const AppProvider: FC = observer((props) => { const { config: { envConfig }, } = useApplication(); + // themes + const { resolvedTheme } = useTheme(); return ( - - - - - - - {children} - - - - - - + <> + {/* TODO: Need to handle custom themes for toast */} + + + + + + {children} + + + + + ); }); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx index ee1be4ebb..6e74de061 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx @@ -3,7 +3,6 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react"; import useSWR from "swr"; // hooks -import useToast from "hooks/use-toast"; import { useIssueDetail, useIssues, useProject, useUser } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; @@ -12,7 +11,7 @@ import { IssueDetailRoot } from "components/issues"; import { ProjectArchivedIssueDetailsHeader } from "components/headers"; import { PageHead } from "components/core"; // ui -import { ArchiveIcon, Button, Loader } from "@plane/ui"; +import { ArchiveIcon, Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { RotateCcw } from "lucide-react"; // types @@ -35,7 +34,6 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => { const { issues: { restoreIssue }, } = useIssues(EIssuesStoreType.ARCHIVED); - const { setToastAlert } = useToast(); const { getProjectById } = useProject(); const { membership: { currentProjectRole }, @@ -66,8 +64,8 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => { await restoreIssue(workspaceSlug.toString(), projectId.toString(), archivedIssueId.toString()) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success", message: issue && @@ -78,8 +76,8 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => { router.push(`/${workspaceSlug}/projects/${projectId}/issues/${archivedIssueId}`); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong. Please try again.", }); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx index bee4fc9c7..c44f6186e 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx @@ -8,7 +8,6 @@ import { Controller, useForm } from "react-hook-form"; import { useApplication, usePage, useUser, useWorkspace } from "hooks/store"; import useReloadConfirmations from "hooks/use-reload-confirmation"; -import useToast from "hooks/use-toast"; // services import { FileService } from "services/file.service"; // layouts @@ -18,7 +17,7 @@ import { GptAssistantPopover, PageHead } from "components/core"; import { PageDetailsHeader } from "components/headers/page-details"; // ui import { DocumentEditorWithRef, DocumentReadOnlyEditorWithRef } from "@plane/document-editor"; -import { Spinner } from "@plane/ui"; +import { Spinner, TOAST_TYPE, setToast } from "@plane/ui"; // assets // helpers // types @@ -53,8 +52,6 @@ const PageDetailsPage: NextPageWithLayout = observer(() => { currentUser, membership: { currentProjectRole }, } = useUser(); - // toast alert - const { setToastAlert } = useToast(); const { handleSubmit, setValue, watch, getValues, control, reset } = useForm({ defaultValues: { name: "", description_html: "" }, @@ -148,10 +145,10 @@ const PageDetailsPage: NextPageWithLayout = observer(() => { message: string; type: "success" | "error" | "warning" | "info"; }) => { - setToastAlert({ + setToast({ title, message, - type, + type: type as TOAST_TYPE, }); }; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx index 8c4780cba..1cefb9418 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx @@ -6,8 +6,8 @@ import { useProject, useUser } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; -// hooks -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { AutoArchiveAutomation, AutoCloseAutomation } from "components/automation"; import { PageHead } from "components/core"; @@ -22,8 +22,6 @@ const AutomationSettingsPage: NextPageWithLayout = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // toast alert - const { setToastAlert } = useToast(); // store hooks const { membership: { currentProjectRole }, @@ -34,8 +32,8 @@ const AutomationSettingsPage: NextPageWithLayout = observer(() => { if (!workspaceSlug || !projectId || !projectDetails) return; await updateProject(workspaceSlug.toString(), projectId.toString(), formData).catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong. Please try again.", }); diff --git a/web/pages/[workspaceSlug]/settings/members.tsx b/web/pages/[workspaceSlug]/settings/members.tsx index b8739ae77..f635588c2 100644 --- a/web/pages/[workspaceSlug]/settings/members.tsx +++ b/web/pages/[workspaceSlug]/settings/members.tsx @@ -4,7 +4,6 @@ import { observer } from "mobx-react-lite"; import { Search } from "lucide-react"; // hooks import { useEventTracker, useMember, useUser, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; @@ -13,7 +12,7 @@ import { WorkspaceSettingHeader } from "components/headers"; import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "components/workspace"; import { PageHead } from "components/core"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; import { IWorkspaceBulkInviteFormData } from "@plane/types"; @@ -39,8 +38,6 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => { workspace: { inviteMembersToWorkspace }, } = useMember(); const { currentWorkspace } = useWorkspace(); - // toast alert - const { setToastAlert } = useToast(); const handleWorkspaceInvite = (data: IWorkspaceBulkInviteFormData) => { if (!workspaceSlug) return; @@ -59,8 +56,8 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => { state: "SUCCESS", element: "Workspace settings member page", }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Invitations sent successfully.", }); @@ -77,8 +74,8 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => { state: "FAILED", element: "Workspace settings member page", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: `${err.error ?? "Something went wrong. Please try again."}`, }); diff --git a/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx b/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx index 60e65e905..bafaa3aaa 100644 --- a/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx +++ b/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx @@ -7,14 +7,12 @@ import { useUser, useWebhook, useWorkspace } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; -// hooks -import useToast from "hooks/use-toast"; // components import { WorkspaceSettingHeader } from "components/headers"; import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "components/web-hooks"; import { PageHead } from "components/core"; // ui -import { Spinner } from "@plane/ui"; +import { Spinner, TOAST_TYPE, setToast } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; import { IWebhook } from "@plane/types"; @@ -31,8 +29,6 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => { } = useUser(); const { currentWebhook, fetchWebhookById, updateWebhook } = useWebhook(); const { currentWorkspace } = useWorkspace(); - // toast - const { setToastAlert } = useToast(); // TODO: fix this error // useEffect(() => { @@ -62,15 +58,15 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => { }; await updateWebhook(workspaceSlug.toString(), formData.id, payload) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Webhook updated successfully.", }); }) .catch((error) => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error?.error ?? "Something went wrong. Please try again.", }); diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index 75023d36e..bc8230256 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -1,12 +1,14 @@ import { ReactElement } from "react"; import Head from "next/head"; import { AppProps } from "next/app"; +import { ThemeProvider } from "next-themes"; // styles import "styles/globals.css"; import "styles/command-pallette.css"; import "styles/nprogress.css"; import "styles/react-day-picker.css"; // constants +import { THEMES } from "constants/themes"; import { SITE_TITLE } from "constants/seo-variables"; // mobx store provider import { StoreProvider } from "contexts/store-context"; @@ -29,7 +31,9 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { {SITE_TITLE} - {getLayout()} + + {getLayout()} + ); diff --git a/web/pages/_error.tsx b/web/pages/_error.tsx index 11a7ee852..0a530cf9f 100644 --- a/web/pages/_error.tsx +++ b/web/pages/_error.tsx @@ -4,12 +4,10 @@ import { useRouter } from "next/router"; // services import { AuthService } from "services/auth.service"; -// hooks -import useToast from "hooks/use-toast"; // layouts import DefaultLayout from "layouts/default-layout"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // services const authService = new AuthService(); @@ -17,14 +15,12 @@ const authService = new AuthService(); const CustomErrorComponent = () => { const router = useRouter(); - const { setToastAlert } = useToast(); - const handleSignOut = async () => { await authService .signOut() .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Failed to sign out. Please try again.", }) diff --git a/web/pages/accounts/forgot-password.tsx b/web/pages/accounts/forgot-password.tsx index 0eef16009..e167fc037 100644 --- a/web/pages/accounts/forgot-password.tsx +++ b/web/pages/accounts/forgot-password.tsx @@ -5,7 +5,6 @@ import { Controller, useForm } from "react-hook-form"; // services import { AuthService } from "services/auth.service"; // hooks -import useToast from "hooks/use-toast"; import useTimer from "hooks/use-timer"; import { useEventTracker } from "hooks/store"; // layouts @@ -14,7 +13,7 @@ import DefaultLayout from "layouts/default-layout"; import { LatestFeatureBlock } from "components/common"; import { PageHead } from "components/core"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // images import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; // helpers @@ -40,8 +39,6 @@ const ForgotPasswordPage: NextPageWithLayout = () => { const { email } = router.query; // store hooks const { captureEvent } = useEventTracker(); - // toast - const { setToastAlert } = useToast(); // timer const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(0); // form info @@ -65,8 +62,8 @@ const ForgotPasswordPage: NextPageWithLayout = () => { captureEvent(FORGOT_PASS_LINK, { state: "SUCCESS", }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Email sent", message: "Check your inbox for a link to reset your password. If it doesn't appear within a few minutes, check your spam folder.", @@ -77,8 +74,8 @@ const ForgotPasswordPage: NextPageWithLayout = () => { captureEvent(FORGOT_PASS_LINK, { state: "FAILED", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }); diff --git a/web/pages/accounts/reset-password.tsx b/web/pages/accounts/reset-password.tsx index c848245ac..f7a49a19d 100644 --- a/web/pages/accounts/reset-password.tsx +++ b/web/pages/accounts/reset-password.tsx @@ -5,7 +5,6 @@ import { Controller, useForm } from "react-hook-form"; // services import { AuthService } from "services/auth.service"; // hooks -import useToast from "hooks/use-toast"; import useSignInRedirection from "hooks/use-sign-in-redirection"; import { useEventTracker } from "hooks/store"; // layouts @@ -14,7 +13,7 @@ import DefaultLayout from "layouts/default-layout"; import { LatestFeatureBlock } from "components/common"; import { PageHead } from "components/core"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // images import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; // helpers @@ -47,8 +46,6 @@ const ResetPasswordPage: NextPageWithLayout = () => { const [showPassword, setShowPassword] = useState(false); // store hooks const { captureEvent } = useEventTracker(); - // toast - const { setToastAlert } = useToast(); // sign in redirection hook const { handleRedirection } = useSignInRedirection(); // form info @@ -82,8 +79,8 @@ const ResetPasswordPage: NextPageWithLayout = () => { captureEvent(NEW_PASS_CREATED, { state: "FAILED", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }); diff --git a/web/pages/god-mode/authorization.tsx b/web/pages/god-mode/authorization.tsx index e36a1a455..6274fca20 100644 --- a/web/pages/god-mode/authorization.tsx +++ b/web/pages/god-mode/authorization.tsx @@ -8,10 +8,8 @@ import { InstanceAdminLayout } from "layouts/admin-layout"; import { NextPageWithLayout } from "lib/types"; // hooks import { useApplication } from "hooks/store"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Loader, ToggleSwitch } from "@plane/ui"; +import { Loader, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; // components import { InstanceGithubConfigForm, InstanceGoogleConfigForm } from "components/instance"; import { PageHead } from "components/core"; @@ -24,9 +22,6 @@ const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => { useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations()); - // toast - const { setToastAlert } = useToast(); - // state const [isSubmitting, setIsSubmitting] = useState(false); @@ -46,18 +41,18 @@ const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => { await updateInstanceConfigurations(payload) .then(() => { - setToastAlert({ + setToast({ title: "Success", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "SSO and OAuth Settings updated successfully", }); setIsSubmitting(false); }) .catch((err) => { console.error(err); - setToastAlert({ + setToast({ title: "Error", - type: "error", + type: TOAST_TYPE.ERROR, message: "Failed to update SSO and OAuth Settings", }); setIsSubmitting(false); diff --git a/web/pages/invitations/index.tsx b/web/pages/invitations/index.tsx index b5acec196..18441f0a0 100644 --- a/web/pages/invitations/index.tsx +++ b/web/pages/invitations/index.tsx @@ -11,12 +11,11 @@ import { WorkspaceService } from "services/workspace.service"; import { UserService } from "services/user.service"; // hooks import { useEventTracker, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // layouts import DefaultLayout from "layouts/default-layout"; import { UserAuthWrapper } from "layouts/auth-layout"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // images import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg"; import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg"; @@ -48,8 +47,6 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { const router = useRouter(); // next-themes const { theme } = useTheme(); - // toast alert - const { setToastAlert } = useToast(); const { data: invitations } = useSWR("USER_WORKSPACE_INVITATIONS", () => workspaceService.userWorkspaceInvitations()); @@ -68,8 +65,8 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { const submitInvitations = () => { if (invitationsRespond.length === 0) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please select at least one invitation.", }); @@ -101,8 +98,8 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { router.push(`/${redirectWorkspace?.slug}`); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong, Please try again.", }); @@ -116,8 +113,8 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { state: "FAILED", element: "Workspace invitations page", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong, Please try again.", }); diff --git a/web/pages/profile/change-password.tsx b/web/pages/profile/change-password.tsx index 80e2965d6..f37a2b6a6 100644 --- a/web/pages/profile/change-password.tsx +++ b/web/pages/profile/change-password.tsx @@ -8,12 +8,10 @@ import { useApplication, useUser } from "hooks/store"; import { UserService } from "services/user.service"; // components import { PageHead } from "components/core"; -// hooks -import useToast from "hooks/use-toast"; // layout import { ProfileSettingsLayout } from "layouts/settings-layout"; // ui -import { Button, Input, Spinner } from "@plane/ui"; +import { Button, Input, Spinner, TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; @@ -46,33 +44,28 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => { handleSubmit, formState: { errors, isSubmitting }, } = useForm({ defaultValues }); - const { setToastAlert } = useToast(); const handleChangePassword = async (formData: FormValues) => { if (formData.new_password !== formData.confirm_password) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "The new password and the confirm password don't match.", }); return; } - await userService - .changePassword(formData) - .then(() => { - setToastAlert({ - type: "success", - title: "Success!", - message: "Password changed successfully.", - }); - }) - .catch((error) => { - setToastAlert({ - type: "error", - title: "Error!", - message: error?.error ?? "Something went wrong. Please try again.", - }); - }); + const changePasswordPromise = userService.changePassword(formData); + setPromiseToast(changePasswordPromise, { + loading: "Changing password...", + success: { + title: "Success!", + message: () => "Password changed successfully.", + }, + error: { + title: "Error!", + message: () => "Something went wrong. Please try again.", + }, + }); }; useEffect(() => { diff --git a/web/pages/profile/index.tsx b/web/pages/profile/index.tsx index c4eab324a..dc653cba9 100644 --- a/web/pages/profile/index.tsx +++ b/web/pages/profile/index.tsx @@ -7,14 +7,22 @@ import { FileService } from "services/file.service"; // hooks import { useApplication, useUser } from "hooks/store"; import useUserAuth from "hooks/use-user-auth"; -import useToast from "hooks/use-toast"; // layouts import { ProfileSettingsLayout } from "layouts/settings-layout"; // components import { ImagePickerPopover, UserImageUploadModal, PageHead } from "components/core"; import { DeactivateAccountModal } from "components/account"; // ui -import { Button, CustomSelect, CustomSearchSelect, Input, Spinner } from "@plane/ui"; +import { + Button, + CustomSelect, + CustomSearchSelect, + Input, + Spinner, + TOAST_TYPE, + setPromiseToast, + setToast, +} from "@plane/ui"; // icons import { ChevronDown, User2 } from "lucide-react"; // types @@ -52,8 +60,6 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { control, formState: { errors }, } = useForm({ defaultValues }); - // toast alert - const { setToastAlert } = useToast(); // store hooks const { currentUser: myProfile, updateCurrentUser, currentUserLoader } = useUser(); // custom hooks @@ -76,24 +82,22 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { user_timezone: formData.user_timezone, }; - await updateCurrentUser(payload) - .then(() => { - setToastAlert({ - type: "success", - title: "Success!", - message: "Profile updated successfully.", - }); - }) - .catch(() => - setToastAlert({ - type: "error", - title: "Error!", - message: "There was some error in updating your profile. Please try again.", - }) - ); - setTimeout(() => { - setIsLoading(false); - }, 300); + const updateCurrentUserDetail = updateCurrentUser(payload).finally(() => setIsLoading(false)); + setPromiseToast(updateCurrentUserDetail, { + loading: "Updating...", + success: { + title: "Success!", + message: () => `Profile updated successfully.`, + }, + error: { + title: "Error!", + message: () => `There was some error in updating your profile. Please try again.`, + }, + }); + + // setTimeout(() => { + // setIsLoading(false); + // }, 300); }; const handleDelete = (url: string | null | undefined, updateUser: boolean = false) => { @@ -105,16 +109,16 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { if (updateUser) updateCurrentUser({ avatar: "" }) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", - message: "Profile picture removed successfully.", + message: "Profile picture deleted successfully.", }); setIsRemoving(false); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "There was some error in deleting your profile picture. Please try again.", }); diff --git a/web/pages/profile/preferences/theme.tsx b/web/pages/profile/preferences/theme.tsx index 134ace79e..94540aeda 100644 --- a/web/pages/profile/preferences/theme.tsx +++ b/web/pages/profile/preferences/theme.tsx @@ -3,13 +3,12 @@ import { observer } from "mobx-react-lite"; import { useTheme } from "next-themes"; // hooks import { useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // layouts import { ProfilePreferenceSettingsLayout } from "layouts/settings-layout/profile/preferences"; // components import { CustomThemeSelector, ThemeSwitch, PageHead } from "components/core"; // ui -import { Spinner } from "@plane/ui"; +import { Spinner, setPromiseToast } from "@plane/ui"; // constants import { I_THEME_OPTION, THEME_OPTIONS } from "constants/themes"; // type @@ -24,7 +23,6 @@ const ProfilePreferencesThemePage: NextPageWithLayout = observer(() => { const userTheme = currentUser?.theme; // hooks const { setTheme } = useTheme(); - const { setToastAlert } = useToast(); useEffect(() => { if (userTheme) { @@ -37,11 +35,18 @@ const ProfilePreferencesThemePage: NextPageWithLayout = observer(() => { const handleThemeChange = (themeOption: I_THEME_OPTION) => { setTheme(themeOption.value); - updateCurrentUserTheme(themeOption.value).catch(() => { - setToastAlert({ - title: "Failed to Update the theme", - type: "error", - }); + const updateCurrentUserThemePromise = updateCurrentUserTheme(themeOption.value); + + setPromiseToast(updateCurrentUserThemePromise, { + loading: "Updating theme...", + success: { + title: "Success!", + message: () => "Theme updated successfully!", + }, + error: { + title: "Error!", + message: () => "Failed to Update the theme", + }, }); }; diff --git a/web/styles/globals.css b/web/styles/globals.css index e4de1a3da..6c51e75c4 100644 --- a/web/styles/globals.css +++ b/web/styles/globals.css @@ -149,6 +149,27 @@ --color-onboarding-border-300: 229, 229, 229, 0.5; --color-onboarding-shadow-sm: 0px 4px 20px 0px rgba(126, 139, 171, 0.1); + + /* toast theme */ + --color-toast-success-text: 62, 155, 79; + --color-toast-error-text: 220, 62, 66; + --color-toast-warning-text: 255, 186, 24; + --color-toast-info-text: 51, 88, 212; + --color-toast-loading-text: 28, 32, 36; + --color-toast-secondary-text: 128, 131, 141; + --color-toast-tertiary-text: 96, 100, 108; + + --color-toast-success-background: 253, 253, 254; + --color-toast-error-background: 255, 252, 252; + --color-toast-warning-background: 254, 253, 251; + --color-toast-info-background: 253, 253, 254; + --color-toast-loading-background: 253, 253, 254; + + --color-toast-success-border: 218, 241, 219; + --color-toast-error-border: 255, 219, 220; + --color-toast-warning-border: 255, 247, 194; + --color-toast-info-border: 210, 222, 255; + --color-toast-loading-border: 224, 225, 230; } [data-theme="light-contrast"] { @@ -217,6 +238,27 @@ --color-onboarding-border-300: 34, 35, 38, 0.5; --color-onboarding-shadow-sm: 0px 4px 20px 0px rgba(39, 44, 56, 0.1); + + /* toast theme */ + --color-toast-success-text: 178, 221, 181; + --color-toast-error-text: 206, 44, 49; + --color-toast-warning-text: 255, 186, 24; + --color-toast-info-text: 141, 164, 239; + --color-toast-loading-text: 255, 255, 255; + --color-toast-secondary-text: 185, 187, 198; + --color-toast-tertiary-text: 139, 141, 152; + + --color-toast-success-background: 46, 46, 46; + --color-toast-error-background: 46, 46, 46; + --color-toast-warning-background: 46, 46, 46; + --color-toast-info-background: 46, 46, 46; + --color-toast-loading-background: 46, 46, 46; + + --color-toast-success-border: 42, 126, 59; + --color-toast-error-border: 100, 23, 35; + --color-toast-warning-border: 79, 52, 34; + --color-toast-info-border: 58, 91, 199; + --color-toast-loading-border: 96, 100, 108; } [data-theme="dark-contrast"] { diff --git a/yarn.lock b/yarn.lock index f413d1a44..81e6224e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8107,6 +8107,11 @@ snake-case@^3.0.4: dot-case "^3.0.4" tslib "^2.0.3" +sonner@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/sonner/-/sonner-1.4.2.tgz#92740c293e9d911de726080995bd8a0cc677ccd1" + integrity sha512-x3Kfzfhb56V/ErvUnH5dZcsu6QkZpyIlRAogO4vAbN+AkBsA/8CFqOV+5djqbE5pQCpejtO4JBWL1zRj2sO/Vg== + source-list-map@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" From 87eadc3c5d4d6f349d95307b551bb8d8448e828e Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:21:07 +0530 Subject: [PATCH 032/308] chore: issue link model field change (#3852) --- apiserver/plane/api/serializers/issue.py | 28 ++++++++++++++++++- apiserver/plane/app/serializers/issue.py | 27 ++++++++++++++++++ .../db/migrations/0061_alter_issuelink_url.py | 18 ++++++++++++ apiserver/plane/db/models/issue.py | 2 +- 4 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 apiserver/plane/db/migrations/0061_alter_issuelink_url.py diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 4c8d6e815..b8f194b32 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -1,8 +1,9 @@ from lxml import html - # Django imports from django.utils import timezone +from django.core.validators import URLValidator +from django.core.exceptions import ValidationError # Third party imports from rest_framework import serializers @@ -284,6 +285,20 @@ class IssueLinkSerializer(BaseSerializer): "updated_at", ] + def validate_url(self, value): + # Check URL format + validate_url = URLValidator() + try: + validate_url(value) + except ValidationError: + raise serializers.ValidationError("Invalid URL format.") + + # Check URL scheme + if not value.startswith(('http://', 'https://')): + raise serializers.ValidationError("Invalid URL scheme.") + + return value + # Validation if url already exists def create(self, validated_data): if IssueLink.objects.filter( @@ -295,6 +310,17 @@ class IssueLinkSerializer(BaseSerializer): ) return IssueLink.objects.create(**validated_data) + def update(self, instance, validated_data): + if IssueLink.objects.filter( + url=validated_data.get("url"), + issue_id=instance.issue_id, + ).exists(): + raise serializers.ValidationError( + {"error": "URL already exists for this Issue"} + ) + + return super().update(instance, validated_data) + class IssueAttachmentSerializer(BaseSerializer): class Meta: diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index 411c5b73f..1b884bedf 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -1,5 +1,7 @@ # Django imports from django.utils import timezone +from django.core.validators import URLValidator +from django.core.exceptions import ValidationError # Third Party imports from rest_framework import serializers @@ -432,6 +434,20 @@ class IssueLinkSerializer(BaseSerializer): "issue", ] + def validate_url(self, value): + # Check URL format + validate_url = URLValidator() + try: + validate_url(value) + except ValidationError: + raise serializers.ValidationError("Invalid URL format.") + + # Check URL scheme + if not value.startswith(('http://', 'https://')): + raise serializers.ValidationError("Invalid URL scheme.") + + return value + # Validation if url already exists def create(self, validated_data): if IssueLink.objects.filter( @@ -443,6 +459,17 @@ class IssueLinkSerializer(BaseSerializer): ) return IssueLink.objects.create(**validated_data) + def update(self, instance, validated_data): + if IssueLink.objects.filter( + url=validated_data.get("url"), + issue_id=instance.issue_id, + ).exists(): + raise serializers.ValidationError( + {"error": "URL already exists for this Issue"} + ) + + return super().update(instance, validated_data) + class IssueLinkLiteSerializer(BaseSerializer): diff --git a/apiserver/plane/db/migrations/0061_alter_issuelink_url.py b/apiserver/plane/db/migrations/0061_alter_issuelink_url.py new file mode 100644 index 000000000..1aca84a80 --- /dev/null +++ b/apiserver/plane/db/migrations/0061_alter_issuelink_url.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.7 on 2024-03-01 07:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0060_cycle_progress_snapshot'), + ] + + operations = [ + migrations.AlterField( + model_name='issuelink', + name='url', + field=models.TextField(), + ), + ] diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index d5ed4247a..5bd0b3397 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -320,7 +320,7 @@ class IssueAssignee(ProjectBaseModel): class IssueLink(ProjectBaseModel): title = models.CharField(max_length=255, null=True, blank=True) - url = models.URLField() + url = models.TextField() issue = models.ForeignKey( "db.Issue", on_delete=models.CASCADE, related_name="issue_link" ) From 126d01bdc5313a715cefb72d066efe4a5919891e Mon Sep 17 00:00:00 2001 From: "M. Palanikannan" <73993394+Palanikannan1437@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:22:02 +0530 Subject: [PATCH 033/308] [WEB-617] fix: link behaviour fixed on formatting (#3855) * fix: link behaviour fixed on formatting * chore: added harmful script checks for links --- .../custom-link/helpers/clickHandler.ts | 14 ++- .../custom-link/helpers/pasteHandler.ts | 10 +- .../custom-link/{index.tsx => index.ts} | 93 ++++++++++++------- 3 files changed, 71 insertions(+), 46 deletions(-) rename packages/editor/core/src/ui/extensions/custom-link/{index.tsx => index.ts} (69%) diff --git a/packages/editor/core/src/ui/extensions/custom-link/helpers/clickHandler.ts b/packages/editor/core/src/ui/extensions/custom-link/helpers/clickHandler.ts index 0854092a9..ec6c540da 100644 --- a/packages/editor/core/src/ui/extensions/custom-link/helpers/clickHandler.ts +++ b/packages/editor/core/src/ui/extensions/custom-link/helpers/clickHandler.ts @@ -15,9 +15,15 @@ export function clickHandler(options: ClickHandlerOptions): Plugin { return false; } - const eventTarget = event.target as HTMLElement; + let a = event.target as HTMLElement; + const els = []; - if (eventTarget.nodeName !== "A") { + while (a.nodeName !== "DIV") { + els.push(a); + a = a.parentNode as HTMLElement; + } + + if (!els.find((value) => value.nodeName === "A")) { return false; } @@ -28,9 +34,7 @@ export function clickHandler(options: ClickHandlerOptions): Plugin { const target = link?.target ?? attrs.target; if (link && href) { - if (view.editable) { - window.open(href, target); - } + window.open(href, target); return true; } diff --git a/packages/editor/core/src/ui/extensions/custom-link/helpers/pasteHandler.ts b/packages/editor/core/src/ui/extensions/custom-link/helpers/pasteHandler.ts index 83e38054c..475bf28d9 100644 --- a/packages/editor/core/src/ui/extensions/custom-link/helpers/pasteHandler.ts +++ b/packages/editor/core/src/ui/extensions/custom-link/helpers/pasteHandler.ts @@ -33,16 +33,8 @@ export function pasteHandler(options: PasteHandlerOptions): Plugin { return false; } - const html = event.clipboardData?.getData("text/html"); - - const hrefRegex = /href="([^"]*)"/; - - const existingLink = html?.match(hrefRegex); - - const url = existingLink ? existingLink[1] : link.href; - options.editor.commands.setMark(options.type, { - href: url, + href: link.href, }); return true; diff --git a/packages/editor/core/src/ui/extensions/custom-link/index.tsx b/packages/editor/core/src/ui/extensions/custom-link/index.ts similarity index 69% rename from packages/editor/core/src/ui/extensions/custom-link/index.tsx rename to packages/editor/core/src/ui/extensions/custom-link/index.ts index e66d18904..88e7abfe5 100644 --- a/packages/editor/core/src/ui/extensions/custom-link/index.tsx +++ b/packages/editor/core/src/ui/extensions/custom-link/index.ts @@ -1,41 +1,76 @@ -import { Mark, markPasteRule, mergeAttributes } from "@tiptap/core"; +import { Mark, markPasteRule, mergeAttributes, PasteRuleMatch } from "@tiptap/core"; import { Plugin } from "@tiptap/pm/state"; import { find, registerCustomProtocol, reset } from "linkifyjs"; - -import { autolink } from "src/ui/extensions/custom-link/helpers/autolink"; -import { clickHandler } from "src/ui/extensions/custom-link/helpers/clickHandler"; -import { pasteHandler } from "src/ui/extensions/custom-link/helpers/pasteHandler"; +import { autolink } from "./helpers/autolink"; +import { clickHandler } from "./helpers/clickHandler"; +import { pasteHandler } from "./helpers/pasteHandler"; export interface LinkProtocolOptions { scheme: string; optionalSlashes?: boolean; } +export const pasteRegex = + /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z]{2,}\b(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)/gi; + export interface LinkOptions { + /** + * If enabled, it adds links as you type. + */ autolink: boolean; - inclusive: boolean; + /** + * An array of custom protocols to be registered with linkifyjs. + */ protocols: Array; + /** + * If enabled, links will be opened on click. + */ openOnClick: boolean; + /** + * If enabled, links will be inclusive i.e. if you move your cursor to the + * link text, and start typing, it'll be a part of the link itself. + */ + inclusive: boolean; + /** + * Adds a link to the current selection if the pasted content only contains an url. + */ linkOnPaste: boolean; + /** + * A list of HTML attributes to be rendered. + */ HTMLAttributes: Record; + /** + * A validation function that modifies link verification for the auto linker. + * @param url - The url to be validated. + * @returns - True if the url is valid, false otherwise. + */ validate?: (url: string) => boolean; } declare module "@tiptap/core" { interface Commands { link: { + /** + * Set a link mark + */ setLink: (attributes: { href: string; target?: string | null; rel?: string | null; class?: string | null; }) => ReturnType; + /** + * Toggle a link mark + */ toggleLink: (attributes: { href: string; target?: string | null; rel?: string | null; class?: string | null; }) => ReturnType; + /** + * Unset a link mark + */ unsetLink: () => ReturnType; }; } @@ -150,37 +185,31 @@ export const CustomLinkExtension = Mark.create({ addPasteRules() { return [ markPasteRule({ - find: (text) => - find(text) - .filter((link) => { - if (this.options.validate) { - return this.options.validate(link.value); - } - return true; - }) - .filter((link) => link.isLink) - .map((link) => ({ - text: link.value, - index: link.start, - data: link, - })), - type: this.type, - getAttributes: (match, pasteEvent) => { - const html = pasteEvent?.clipboardData?.getData("text/html"); - const hrefRegex = /href="([^"]*)"/; + find: (text) => { + const foundLinks: PasteRuleMatch[] = []; - const existingLink = html?.match(hrefRegex); + if (text) { + const links = find(text).filter((item) => item.isLink); - if (existingLink) { - return { - href: existingLink[1], - }; + if (links.length) { + links.forEach((link) => + foundLinks.push({ + text: link.value, + data: { + href: link.href, + }, + index: link.start, + }) + ); + } } - return { - href: match.data?.href, - }; + return foundLinks; }, + type: this.type, + getAttributes: (match) => ({ + href: match.data?.href, + }), }), ]; }, From 5a32d10f96b2961739280e3dcee62a7cba95186e Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:24:36 +0530 Subject: [PATCH 034/308] [WEB-373] chore: new dashboard updates (#3849) * chore: replaced marimekko graph with a bar graph * chore: add bar onClick handler * chore: custom date filter for widgets * style: priority graph * chore: workspace profile activity pagination * chore: profile activity pagination * chore: user profile activity pagination * chore: workspace user activity csv download * chore: download activity button added * chore: workspace user pagination * chore: collabrator pagination * chore: field change * chore: recent collaborators pagination * chore: changed the collabrators * chore: collabrators list changed * fix: distinct users * chore: search filter in collaborators * fix: import error * chore: update priority graph x-axis values * chore: admin and member request validation * chore: update csv download request method * chore: search implementation for the collaborators widget * refactor: priority distribution card * chore: add enum for duration filters * chore: update inbox types * chore: add todos for refactoring --------- Co-authored-by: NarayanBavisetti --- apiserver/plane/app/urls/workspace.py | 6 + apiserver/plane/app/views/__init__.py | 1 + apiserver/plane/app/views/dashboard.py | 188 ++++++++-------- apiserver/plane/app/views/workspace.py | 63 ++++++ packages/types/src/cycles.d.ts | 9 +- .../types/src/{ => dashboard}/dashboard.d.ts | 49 +++-- packages/types/src/dashboard/enums.ts | 8 + packages/types/src/dashboard/index.ts | 2 + packages/types/src/enums.ts | 6 + .../{inbox.d.ts => inbox/inbox-types.d.ts} | 32 +-- packages/types/src/inbox/root.d.ts | 3 +- packages/types/src/index.d.ts | 4 +- packages/types/src/modules.d.ts | 28 +-- packages/types/src/users.d.ts | 26 +-- .../core/filters/date-filter-select.tsx | 11 +- .../dashboard/widgets/assigned-issues.tsx | 23 +- .../dashboard/widgets/created-issues.tsx | 23 +- .../widgets/dropdowns/duration-filter.tsx | 56 +++-- .../widgets/issue-panels/tabs-list.tsx | 6 +- .../dashboard/widgets/issues-by-priority.tsx | 149 +++---------- .../widgets/issues-by-state-group.tsx | 21 +- .../widgets/loaders/recent-collaborators.tsx | 15 +- .../dashboard/widgets/recent-activity.tsx | 15 +- .../widgets/recent-collaborators.tsx | 94 -------- .../collaborators-list.tsx | 120 ++++++++++ .../recent-collaborators/default-list.tsx | 59 +++++ .../widgets/recent-collaborators/index.ts | 1 + .../widgets/recent-collaborators/root.tsx | 48 ++++ .../recent-collaborators/search-list.tsx | 80 +++++++ web/components/graphs/index.ts | 1 + web/components/graphs/issues-by-priority.tsx | 103 +++++++++ web/components/inbox/inbox-issue-actions.tsx | 4 +- .../profile/activity/activity-list.tsx | 162 ++++++++++++++ .../profile/activity/download-button.tsx | 57 +++++ web/components/profile/activity/index.ts | 4 + .../activity/profile-activity-list.tsx | 190 ++++++++++++++++ .../activity/workspace-activity-list.tsx | 50 +++++ web/components/profile/index.ts | 1 + web/components/profile/navbar.tsx | 9 +- web/components/profile/overview/activity.tsx | 9 +- .../overview/priority-distribution.tsx | 88 -------- .../overview/priority-distribution/index.ts | 1 + .../priority-distribution/main-content.tsx | 31 +++ .../priority-distribution.tsx | 33 +++ .../profile/overview/state-distribution.tsx | 4 +- web/components/profile/overview/workload.tsx | 6 +- web/components/profile/sidebar.tsx | 4 +- web/components/ui/graphs/index.ts | 1 - web/components/ui/graphs/marimekko-graph.tsx | 48 ---- web/constants/dashboard.ts | 20 +- web/constants/fetch-keys.ts | 11 +- web/constants/profile.ts | 5 + web/helpers/dashboard.helper.ts | 40 +++- web/layouts/user-profile-layout/layout.tsx | 16 +- web/package.json | 1 - .../profile/[userId]/activity.tsx | 84 +++++++ .../profile/[userId]/index.tsx | 2 +- web/pages/profile/activity.tsx | 207 ++++-------------- web/services/user.service.ts | 44 ++-- web/store/user/index.ts | 18 -- yarn.lock | 13 -- 61 files changed, 1568 insertions(+), 845 deletions(-) rename packages/types/src/{ => dashboard}/dashboard.d.ts (79%) create mode 100644 packages/types/src/dashboard/enums.ts create mode 100644 packages/types/src/dashboard/index.ts create mode 100644 packages/types/src/enums.ts rename packages/types/src/{inbox.d.ts => inbox/inbox-types.d.ts} (65%) delete mode 100644 web/components/dashboard/widgets/recent-collaborators.tsx create mode 100644 web/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx create mode 100644 web/components/dashboard/widgets/recent-collaborators/default-list.tsx create mode 100644 web/components/dashboard/widgets/recent-collaborators/index.ts create mode 100644 web/components/dashboard/widgets/recent-collaborators/root.tsx create mode 100644 web/components/dashboard/widgets/recent-collaborators/search-list.tsx create mode 100644 web/components/graphs/index.ts create mode 100644 web/components/graphs/issues-by-priority.tsx create mode 100644 web/components/profile/activity/activity-list.tsx create mode 100644 web/components/profile/activity/download-button.tsx create mode 100644 web/components/profile/activity/index.ts create mode 100644 web/components/profile/activity/profile-activity-list.tsx create mode 100644 web/components/profile/activity/workspace-activity-list.tsx delete mode 100644 web/components/profile/overview/priority-distribution.tsx create mode 100644 web/components/profile/overview/priority-distribution/index.ts create mode 100644 web/components/profile/overview/priority-distribution/main-content.tsx create mode 100644 web/components/profile/overview/priority-distribution/priority-distribution.tsx delete mode 100644 web/components/ui/graphs/marimekko-graph.tsx create mode 100644 web/pages/[workspaceSlug]/profile/[userId]/activity.tsx diff --git a/apiserver/plane/app/urls/workspace.py b/apiserver/plane/app/urls/workspace.py index a70ff18e5..8b21bb9e1 100644 --- a/apiserver/plane/app/urls/workspace.py +++ b/apiserver/plane/app/urls/workspace.py @@ -22,6 +22,7 @@ from plane.app.views import ( WorkspaceUserPropertiesEndpoint, WorkspaceStatesEndpoint, WorkspaceEstimatesEndpoint, + ExportWorkspaceUserActivityEndpoint, WorkspaceModulesEndpoint, WorkspaceCyclesEndpoint, ) @@ -191,6 +192,11 @@ urlpatterns = [ WorkspaceUserActivityEndpoint.as_view(), name="workspace-user-activity", ), + path( + "workspaces//user-activity//export/", + ExportWorkspaceUserActivityEndpoint.as_view(), + name="export-workspace-user-activity", + ), path( "workspaces//user-profile//", WorkspaceUserProfileEndpoint.as_view(), diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index d4a13e497..6af60ff9c 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -49,6 +49,7 @@ from .workspace import ( WorkspaceUserPropertiesEndpoint, WorkspaceStatesEndpoint, WorkspaceEstimatesEndpoint, + ExportWorkspaceUserActivityEndpoint, WorkspaceModulesEndpoint, WorkspaceCyclesEndpoint, ) diff --git a/apiserver/plane/app/views/dashboard.py b/apiserver/plane/app/views/dashboard.py index 62ce0d910..9078d2ab5 100644 --- a/apiserver/plane/app/views/dashboard.py +++ b/apiserver/plane/app/views/dashboard.py @@ -14,6 +14,7 @@ from django.db.models import ( JSONField, Func, Prefetch, + IntegerField, ) from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.fields import ArrayField @@ -38,6 +39,8 @@ from plane.db.models import ( IssueLink, IssueAttachment, IssueRelation, + IssueAssignee, + User, ) from plane.app.serializers import ( IssueActivitySerializer, @@ -212,11 +215,11 @@ def dashboard_assigned_issues(self, request, slug): if issue_type == "overdue": overdue_issues_count = assigned_issues.filter( state__group__in=["backlog", "unstarted", "started"], - target_date__lt=timezone.now() + target_date__lt=timezone.now(), ).count() overdue_issues = assigned_issues.filter( state__group__in=["backlog", "unstarted", "started"], - target_date__lt=timezone.now() + target_date__lt=timezone.now(), )[:5] return Response( { @@ -231,11 +234,11 @@ def dashboard_assigned_issues(self, request, slug): if issue_type == "upcoming": upcoming_issues_count = assigned_issues.filter( state__group__in=["backlog", "unstarted", "started"], - target_date__gte=timezone.now() + target_date__gte=timezone.now(), ).count() upcoming_issues = assigned_issues.filter( state__group__in=["backlog", "unstarted", "started"], - target_date__gte=timezone.now() + target_date__gte=timezone.now(), )[:5] return Response( { @@ -365,11 +368,11 @@ def dashboard_created_issues(self, request, slug): if issue_type == "overdue": overdue_issues_count = created_issues.filter( state__group__in=["backlog", "unstarted", "started"], - target_date__lt=timezone.now() + target_date__lt=timezone.now(), ).count() overdue_issues = created_issues.filter( state__group__in=["backlog", "unstarted", "started"], - target_date__lt=timezone.now() + target_date__lt=timezone.now(), )[:5] return Response( { @@ -382,11 +385,11 @@ def dashboard_created_issues(self, request, slug): if issue_type == "upcoming": upcoming_issues_count = created_issues.filter( state__group__in=["backlog", "unstarted", "started"], - target_date__gte=timezone.now() + target_date__gte=timezone.now(), ).count() upcoming_issues = created_issues.filter( state__group__in=["backlog", "unstarted", "started"], - target_date__gte=timezone.now() + target_date__gte=timezone.now(), )[:5] return Response( { @@ -503,7 +506,9 @@ def dashboard_recent_projects(self, request, slug): ).exclude(id__in=unique_project_ids) # Append additional project IDs to the existing list - unique_project_ids.update(additional_projects.values_list("id", flat=True)) + unique_project_ids.update( + additional_projects.values_list("id", flat=True) + ) return Response( list(unique_project_ids)[:4], @@ -512,90 +517,97 @@ def dashboard_recent_projects(self, request, slug): def dashboard_recent_collaborators(self, request, slug): - # Fetch all project IDs where the user belongs to - user_projects = Project.objects.filter( - project_projectmember__member=request.user, - project_projectmember__is_active=True, - workspace__slug=slug, - ).values_list("id", flat=True) - - # Fetch all users who have performed an activity in the projects where the user exists - users_with_activities = ( + # Subquery to count activities for each project member + activity_count_subquery = ( IssueActivity.objects.filter( workspace__slug=slug, - project_id__in=user_projects, + actor=OuterRef("member"), + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, ) .values("actor") - .exclude(actor=request.user) - .annotate(num_activities=Count("actor")) - .order_by("-num_activities") - )[:7] - - # Get the count of active issues for each user in users_with_activities - users_with_active_issues = [] - for user_activity in users_with_activities: - user_id = user_activity["actor"] - active_issue_count = Issue.objects.filter( - assignees__in=[user_id], - state__group__in=["unstarted", "started"], - ).count() - users_with_active_issues.append( - {"user_id": user_id, "active_issue_count": active_issue_count} - ) - - # Insert the logged-in user's ID and their active issue count at the beginning - active_issue_count = Issue.objects.filter( - assignees__in=[request.user], - state__group__in=["unstarted", "started"], - ).count() - - if users_with_activities.count() < 7: - # Calculate the additional collaborators needed - additional_collaborators_needed = 7 - users_with_activities.count() - - # Fetch additional collaborators from the project_member table - additional_collaborators = list( - set( - ProjectMember.objects.filter( - ~Q(member=request.user), - project_id__in=user_projects, - workspace__slug=slug, - ) - .exclude( - member__in=[ - user["actor"] for user in users_with_activities - ] - ) - .values_list("member", flat=True) - ) - ) - - additional_collaborators = additional_collaborators[ - :additional_collaborators_needed - ] - - # Append additional collaborators to the list - for collaborator_id in additional_collaborators: - active_issue_count = Issue.objects.filter( - assignees__in=[collaborator_id], - state__group__in=["unstarted", "started"], - ).count() - users_with_active_issues.append( - { - "user_id": str(collaborator_id), - "active_issue_count": active_issue_count, - } - ) - - users_with_active_issues.insert( - 0, - {"user_id": request.user.id, "active_issue_count": active_issue_count}, + .annotate(num_activities=Count("pk")) + .values("num_activities") ) - return Response(users_with_active_issues, status=status.HTTP_200_OK) + # Get all project members and annotate them with activity counts + project_members_with_activities = ( + ProjectMember.objects.filter( + workspace__slug=slug, + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + .annotate( + num_activities=Coalesce( + Subquery(activity_count_subquery), + Value(0), + output_field=IntegerField(), + ), + is_current_user=Case( + When(member=request.user, then=Value(0)), + default=Value(1), + output_field=IntegerField(), + ), + ) + .values_list("member", flat=True) + .order_by("is_current_user", "-num_activities") + .distinct() + ) + search = request.query_params.get("search", None) + if search: + project_members_with_activities = ( + project_members_with_activities.filter( + Q(member__display_name__icontains=search) + | Q(member__first_name__icontains=search) + | Q(member__last_name__icontains=search) + ) + ) + + return self.paginate( + request=request, + queryset=project_members_with_activities, + controller=self.get_results_controller, + ) class DashboardEndpoint(BaseAPIView): + def get_results_controller(self, project_members_with_activities): + user_active_issue_counts = ( + User.objects.filter(id__in=project_members_with_activities) + .annotate( + active_issue_count=Count( + Case( + When( + issue_assignee__issue__state__group__in=[ + "unstarted", + "started", + ], + then=1, + ), + output_field=IntegerField(), + ) + ) + ) + .values("active_issue_count", user_id=F("id")) + ) + # Create a dictionary to store the active issue counts by user ID + active_issue_counts_dict = { + user["user_id"]: user["active_issue_count"] + for user in user_active_issue_counts + } + + # Preserve the sequence of project members with activities + paginated_results = [ + { + "user_id": member_id, + "active_issue_count": active_issue_counts_dict.get( + member_id, 0 + ), + } + for member_id in project_members_with_activities + ] + return paginated_results + def create(self, request, slug): serializer = DashboardSerializer(data=request.data) if serializer.is_valid(): @@ -622,7 +634,9 @@ class DashboardEndpoint(BaseAPIView): dashboard_type = request.GET.get("dashboard_type", None) if dashboard_type == "home": dashboard, created = Dashboard.objects.get_or_create( - type_identifier=dashboard_type, owned_by=request.user, is_default=True + type_identifier=dashboard_type, + owned_by=request.user, + is_default=True, ) if created: @@ -639,7 +653,9 @@ class DashboardEndpoint(BaseAPIView): updated_dashboard_widgets = [] for widget_key in widgets_to_fetch: - widget = Widget.objects.filter(key=widget_key).values_list("id", flat=True) + widget = Widget.objects.filter( + key=widget_key + ).values_list("id", flat=True) if widget: updated_dashboard_widgets.append( DashboardWidget( diff --git a/apiserver/plane/app/views/workspace.py b/apiserver/plane/app/views/workspace.py index 47de86a1c..84ba125ba 100644 --- a/apiserver/plane/app/views/workspace.py +++ b/apiserver/plane/app/views/workspace.py @@ -1,9 +1,12 @@ # Python imports import jwt +import csv +import io from datetime import date, datetime from dateutil.relativedelta import relativedelta # Django imports +from django.http import HttpResponse from django.db import IntegrityError from django.conf import settings from django.utils import timezone @@ -1238,6 +1241,66 @@ class WorkspaceUserActivityEndpoint(BaseAPIView): ) +class ExportWorkspaceUserActivityEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceEntityPermission, + ] + + def generate_csv_from_rows(self, rows): + """Generate CSV buffer from rows.""" + csv_buffer = io.StringIO() + writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL) + [writer.writerow(row) for row in rows] + csv_buffer.seek(0) + return csv_buffer + + def post(self, request, slug, user_id): + + if not request.data.get("date"): + return Response( + {"error": "Date is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + user_activities = IssueActivity.objects.filter( + ~Q(field__in=["comment", "vote", "reaction", "draft"]), + workspace__slug=slug, + created_at__date=request.data.get("date"), + project__project_projectmember__member=request.user, + actor_id=user_id, + ).select_related("actor", "workspace", "issue", "project")[:10000] + + header = [ + "Actor name", + "Issue ID", + "Project", + "Created at", + "Updated at", + "Action", + "Field", + "Old value", + "New value", + ] + rows = [ + ( + activity.actor.display_name, + f"{activity.project.identifier} - {activity.issue.sequence_id if activity.issue else ''}", + activity.project.name, + activity.created_at, + activity.updated_at, + activity.verb, + activity.field, + activity.old_value, + activity.new_value, + ) + for activity in user_activities + ] + csv_buffer = self.generate_csv_from_rows([header] + rows) + response = HttpResponse(csv_buffer.getvalue(), content_type="text/csv") + response["Content-Disposition"] = 'attachment; filename="workspace-user-activity.csv"' + return response + + class WorkspaceUserProfileEndpoint(BaseAPIView): def get(self, request, slug, user_id): user_data = User.objects.get(pk=user_id) diff --git a/packages/types/src/cycles.d.ts b/packages/types/src/cycles.d.ts index e7ec66ae2..25b7427f5 100644 --- a/packages/types/src/cycles.d.ts +++ b/packages/types/src/cycles.d.ts @@ -1,11 +1,4 @@ -import type { - IUser, - TIssue, - IProjectLite, - IWorkspaceLite, - IIssueFilterOptions, - IUserLite, -} from "@plane/types"; +import type { TIssue, IIssueFilterOptions } from "@plane/types"; export type TCycleView = "all" | "active" | "upcoming" | "completed" | "draft"; diff --git a/packages/types/src/dashboard.d.ts b/packages/types/src/dashboard/dashboard.d.ts similarity index 79% rename from packages/types/src/dashboard.d.ts rename to packages/types/src/dashboard/dashboard.d.ts index 407b5cd79..d565f6688 100644 --- a/packages/types/src/dashboard.d.ts +++ b/packages/types/src/dashboard/dashboard.d.ts @@ -1,7 +1,8 @@ -import { IIssueActivity, TIssuePriorities } from "./issues"; -import { TIssue } from "./issues/issue"; -import { TIssueRelationTypes } from "./issues/issue_relation"; -import { TStateGroups } from "./state"; +import { IIssueActivity, TIssuePriorities } from "../issues"; +import { TIssue } from "../issues/issue"; +import { TIssueRelationTypes } from "../issues/issue_relation"; +import { TStateGroups } from "../state"; +import { EDurationFilters } from "./enums"; export type TWidgetKeys = | "overview_stats" @@ -15,30 +16,27 @@ export type TWidgetKeys = export type TIssuesListTypes = "pending" | "upcoming" | "overdue" | "completed"; -export type TDurationFilterOptions = - | "none" - | "today" - | "this_week" - | "this_month" - | "this_year"; - // widget filters export type TAssignedIssuesWidgetFilters = { - duration?: TDurationFilterOptions; + custom_dates?: string[]; + duration?: EDurationFilters; tab?: TIssuesListTypes; }; export type TCreatedIssuesWidgetFilters = { - duration?: TDurationFilterOptions; + custom_dates?: string[]; + duration?: EDurationFilters; tab?: TIssuesListTypes; }; export type TIssuesByStateGroupsWidgetFilters = { - duration?: TDurationFilterOptions; + duration?: EDurationFilters; + custom_dates?: string[]; }; export type TIssuesByPriorityWidgetFilters = { - duration?: TDurationFilterOptions; + custom_dates?: string[]; + duration?: EDurationFilters; }; export type TWidgetFiltersFormData = @@ -97,6 +95,12 @@ export type TWidgetStatsRequestParams = | { target_date: string; widget_key: "issues_by_priority"; + } + | { + cursor: string; + per_page: number; + search?: string; + widget_key: "recent_collaborators"; }; export type TWidgetIssue = TIssue & { @@ -141,8 +145,17 @@ export type TRecentActivityWidgetResponse = IIssueActivity; export type TRecentProjectsWidgetResponse = string[]; export type TRecentCollaboratorsWidgetResponse = { - active_issue_count: number; - user_id: string; + count: number; + extra_stats: Object | null; + next_cursor: string; + next_page_results: boolean; + prev_cursor: string; + prev_page_results: boolean; + results: { + active_issue_count: number; + user_id: string; + }[]; + total_pages: number; }; export type TWidgetStatsResponse = @@ -153,7 +166,7 @@ export type TWidgetStatsResponse = | TCreatedIssuesWidgetResponse | TRecentActivityWidgetResponse[] | TRecentProjectsWidgetResponse - | TRecentCollaboratorsWidgetResponse[]; + | TRecentCollaboratorsWidgetResponse; // dashboard export type TDashboard = { diff --git a/packages/types/src/dashboard/enums.ts b/packages/types/src/dashboard/enums.ts new file mode 100644 index 000000000..2c9efd5c3 --- /dev/null +++ b/packages/types/src/dashboard/enums.ts @@ -0,0 +1,8 @@ +export enum EDurationFilters { + NONE = "none", + TODAY = "today", + THIS_WEEK = "this_week", + THIS_MONTH = "this_month", + THIS_YEAR = "this_year", + CUSTOM = "custom", +} diff --git a/packages/types/src/dashboard/index.ts b/packages/types/src/dashboard/index.ts new file mode 100644 index 000000000..dec14aea6 --- /dev/null +++ b/packages/types/src/dashboard/index.ts @@ -0,0 +1,2 @@ +export * from "./dashboard"; +export * from "./enums"; diff --git a/packages/types/src/enums.ts b/packages/types/src/enums.ts new file mode 100644 index 000000000..259f13e9b --- /dev/null +++ b/packages/types/src/enums.ts @@ -0,0 +1,6 @@ +export enum EUserProjectRoles { + GUEST = 5, + VIEWER = 10, + MEMBER = 15, + ADMIN = 20, +} diff --git a/packages/types/src/inbox.d.ts b/packages/types/src/inbox/inbox-types.d.ts similarity index 65% rename from packages/types/src/inbox.d.ts rename to packages/types/src/inbox/inbox-types.d.ts index 4d666ae83..9db71c3ee 100644 --- a/packages/types/src/inbox.d.ts +++ b/packages/types/src/inbox/inbox-types.d.ts @@ -1,5 +1,5 @@ -import { TIssue } from "./issues/base"; -import type { IProjectLite } from "./projects"; +import { TIssue } from "../issues/base"; +import type { IProjectLite } from "../projects"; export type TInboxIssueExtended = { completed_at: string | null; @@ -33,34 +33,6 @@ export interface IInbox { workspace: string; } -interface StatePending { - readonly status: -2; -} -interface StatusReject { - status: -1; -} - -interface StatusSnoozed { - status: 0; - snoozed_till: Date; -} - -interface StatusAccepted { - status: 1; -} - -interface StatusDuplicate { - status: 2; - duplicate_to: string; -} - -export type TInboxStatus = - | StatusReject - | StatusSnoozed - | StatusAccepted - | StatusDuplicate - | StatePending; - export interface IInboxFilterOptions { priority?: string[] | null; inbox_status?: number[] | null; diff --git a/packages/types/src/inbox/root.d.ts b/packages/types/src/inbox/root.d.ts index 2f10c088d..6fd21a4fe 100644 --- a/packages/types/src/inbox/root.d.ts +++ b/packages/types/src/inbox/root.d.ts @@ -1,2 +1,3 @@ -export * from "./inbox"; export * from "./inbox-issue"; +export * from "./inbox-types"; +export * from "./inbox"; diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index 6e8ded942..b1eb38a56 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -4,7 +4,6 @@ export * from "./cycles"; export * from "./dashboard"; export * from "./projects"; export * from "./state"; -export * from "./invitation"; export * from "./issues"; export * from "./modules"; export * from "./views"; @@ -15,7 +14,6 @@ export * from "./estimate"; export * from "./importer"; // FIXME: Remove this after development and the refactor/mobx-store-issue branch is stable -export * from "./inbox"; export * from "./inbox/root"; export * from "./analytics"; @@ -32,6 +30,8 @@ export * from "./api_token"; export * from "./instance"; export * from "./app"; +export * from "./enums"; + export type NestedKeyOf = { [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object ? ObjectType[Key] extends { pop: any; push: any } diff --git a/packages/types/src/modules.d.ts b/packages/types/src/modules.d.ts index fcf2d86a2..c532a467c 100644 --- a/packages/types/src/modules.d.ts +++ b/packages/types/src/modules.d.ts @@ -1,16 +1,12 @@ -import type { - IUser, - IUserLite, - TIssue, - IProject, - IWorkspace, - IWorkspaceLite, - IProjectLite, - IIssueFilterOptions, - ILinkDetails, -} from "@plane/types"; +import type { TIssue, IIssueFilterOptions, ILinkDetails } from "@plane/types"; -export type TModuleStatus = "backlog" | "planned" | "in-progress" | "paused" | "completed" | "cancelled"; +export type TModuleStatus = + | "backlog" + | "planned" + | "in-progress" + | "paused" + | "completed" + | "cancelled"; export interface IModule { backlog_issues: number; @@ -68,6 +64,10 @@ export type ModuleLink = { url: string; }; -export type SelectModuleType = (IModule & { actionType: "edit" | "delete" | "create-issue" }) | undefined; +export type SelectModuleType = + | (IModule & { actionType: "edit" | "delete" | "create-issue" }) + | undefined; -export type SelectIssue = (TIssue & { actionType: "edit" | "delete" | "create" }) | undefined; +export type SelectIssue = + | (TIssue & { actionType: "edit" | "delete" | "create" }) + | undefined; diff --git a/packages/types/src/users.d.ts b/packages/types/src/users.d.ts index 81c8abcd5..c428dc7d2 100644 --- a/packages/types/src/users.d.ts +++ b/packages/types/src/users.d.ts @@ -1,5 +1,9 @@ -import { EUserProjectRoles } from "constants/project"; -import { IIssueActivity, IIssueLite, TStateGroups } from "."; +import { + IIssueActivity, + TIssuePriorities, + TStateGroups, + EUserProjectRoles, +} from "."; export interface IUser { id: string; @@ -17,7 +21,6 @@ export interface IUser { is_onboarded: boolean; is_password_autoset: boolean; is_tour_completed: boolean; - is_password_autoset: boolean; mobile_number: string | null; role: string | null; onboarding_step: { @@ -80,7 +83,7 @@ export interface IUserActivity { } export interface IUserPriorityDistribution { - priority: string; + priority: TIssuePriorities; priority_count: number; } @@ -89,21 +92,6 @@ export interface IUserStateDistribution { state_count: number; } -export interface IUserWorkspaceDashboard { - assigned_issues_count: number; - completed_issues_count: number; - issue_activities: IUserActivity[]; - issues_due_week_count: number; - overdue_issues: IIssueLite[]; - completed_issues: { - week_in_month: number; - completed_count: number; - }[]; - pending_issues_count: number; - state_distribution: IUserStateDistribution[]; - upcoming_issues: IIssueLite[]; -} - export interface IUserActivityResponse { count: number; extra_stats: null; diff --git a/web/components/core/filters/date-filter-select.tsx b/web/components/core/filters/date-filter-select.tsx index 9bb10f800..47207e0cc 100644 --- a/web/components/core/filters/date-filter-select.tsx +++ b/web/components/core/filters/date-filter-select.tsx @@ -1,10 +1,7 @@ import React from "react"; - +import { CalendarDays } from "lucide-react"; // ui import { CustomSelect, CalendarAfterIcon, CalendarBeforeIcon } from "@plane/ui"; -// icons -import { CalendarDays } from "lucide-react"; -// fetch-keys type Props = { title: string; @@ -22,17 +19,17 @@ const dueDateRange: DueDate[] = [ { name: "before", value: "before", - icon: , + icon: , }, { name: "after", value: "after", - icon: , + icon: , }, { name: "range", value: "range", - icon: , + icon: , }, ]; diff --git a/web/components/dashboard/widgets/assigned-issues.tsx b/web/components/dashboard/widgets/assigned-issues.tsx index 7c8fbd2a9..407ac9ddf 100644 --- a/web/components/dashboard/widgets/assigned-issues.tsx +++ b/web/components/dashboard/widgets/assigned-issues.tsx @@ -15,7 +15,7 @@ import { // helpers import { getCustomDates, getRedirectionFilters, getTabKey } from "helpers/dashboard.helper"; // types -import { TAssignedIssuesWidgetFilters, TAssignedIssuesWidgetResponse } from "@plane/types"; +import { EDurationFilters, TAssignedIssuesWidgetFilters, TAssignedIssuesWidgetResponse } from "@plane/types"; // constants import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; @@ -30,8 +30,9 @@ export const AssignedIssuesWidget: React.FC = observer((props) => { // derived values const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? "none"; + const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab); + const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; const handleUpdateFilters = async (filters: Partial) => { if (!widgetDetails) return; @@ -43,7 +44,10 @@ export const AssignedIssuesWidget: React.FC = observer((props) => { filters, }); - const filterDates = getCustomDates(filters.duration ?? selectedDurationFilter); + const filterDates = getCustomDates( + filters.duration ?? selectedDurationFilter, + filters.custom_dates ?? selectedCustomDates + ); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, issue_type: filters.tab ?? selectedTab, @@ -53,7 +57,7 @@ export const AssignedIssuesWidget: React.FC = observer((props) => { }; useEffect(() => { - const filterDates = getCustomDates(selectedDurationFilter); + const filterDates = getCustomDates(selectedDurationFilter, selectedCustomDates); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, @@ -81,8 +85,17 @@ export const AssignedIssuesWidget: React.FC = observer((props) => { Assigned to you { + onChange={(val, customDates) => { + if (val === "custom" && customDates) { + handleUpdateFilters({ + duration: val, + custom_dates: customDates, + }); + return; + } + if (val === selectedDurationFilter) return; let newTab = selectedTab; diff --git a/web/components/dashboard/widgets/created-issues.tsx b/web/components/dashboard/widgets/created-issues.tsx index e7832883b..23e7bee27 100644 --- a/web/components/dashboard/widgets/created-issues.tsx +++ b/web/components/dashboard/widgets/created-issues.tsx @@ -15,7 +15,7 @@ import { // helpers import { getCustomDates, getRedirectionFilters, getTabKey } from "helpers/dashboard.helper"; // types -import { TCreatedIssuesWidgetFilters, TCreatedIssuesWidgetResponse } from "@plane/types"; +import { EDurationFilters, TCreatedIssuesWidgetFilters, TCreatedIssuesWidgetResponse } from "@plane/types"; // constants import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; @@ -30,8 +30,9 @@ export const CreatedIssuesWidget: React.FC = observer((props) => { // derived values const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? "none"; + const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab); + const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; const handleUpdateFilters = async (filters: Partial) => { if (!widgetDetails) return; @@ -43,7 +44,10 @@ export const CreatedIssuesWidget: React.FC = observer((props) => { filters, }); - const filterDates = getCustomDates(filters.duration ?? selectedDurationFilter); + const filterDates = getCustomDates( + filters.duration ?? selectedDurationFilter, + filters.custom_dates ?? selectedCustomDates + ); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, issue_type: filters.tab ?? selectedTab, @@ -52,7 +56,7 @@ export const CreatedIssuesWidget: React.FC = observer((props) => { }; useEffect(() => { - const filterDates = getCustomDates(selectedDurationFilter); + const filterDates = getCustomDates(selectedDurationFilter, selectedCustomDates); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, @@ -78,8 +82,17 @@ export const CreatedIssuesWidget: React.FC = observer((props) => { Created by you { + onChange={(val, customDates) => { + if (val === "custom" && customDates) { + handleUpdateFilters({ + duration: val, + custom_dates: customDates, + }); + return; + } + if (val === selectedDurationFilter) return; let newTab = selectedTab; diff --git a/web/components/dashboard/widgets/dropdowns/duration-filter.tsx b/web/components/dashboard/widgets/dropdowns/duration-filter.tsx index 4844ea406..fbdac4f00 100644 --- a/web/components/dashboard/widgets/dropdowns/duration-filter.tsx +++ b/web/components/dashboard/widgets/dropdowns/duration-filter.tsx @@ -1,36 +1,58 @@ +import { useState } from "react"; import { ChevronDown } from "lucide-react"; +// components +import { DateFilterModal } from "components/core"; // ui import { CustomMenu } from "@plane/ui"; +// helpers +import { getDurationFilterDropdownLabel } from "helpers/dashboard.helper"; // types -import { TDurationFilterOptions } from "@plane/types"; +import { EDurationFilters } from "@plane/types"; // constants import { DURATION_FILTER_OPTIONS } from "constants/dashboard"; type Props = { - onChange: (value: TDurationFilterOptions) => void; - value: TDurationFilterOptions; + customDates?: string[]; + onChange: (value: EDurationFilters, customDates?: string[]) => void; + value: EDurationFilters; }; export const DurationFilterDropdown: React.FC = (props) => { - const { onChange, value } = props; + const { customDates, onChange, value } = props; + // states + const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false); return ( - - {DURATION_FILTER_OPTIONS.find((option) => option.key === value)?.label} - -
    - } - placement="bottom-end" - closeOnSelect - > + <> + setIsDateFilterModalOpen(false)} + onSelect={(val) => onChange(EDurationFilters.CUSTOM, val)} + title="Due date" + /> + + {getDurationFilterDropdownLabel(value, customDates ?? [])} + +
    + } + placement="bottom-end" + closeOnSelect + > {DURATION_FILTER_OPTIONS.map((option) => ( - onChange(option.key)}> + { + if (option.key === "custom") setIsDateFilterModalOpen(true); + else onChange(option.key); + }} + > {option.label} ))} - + + ); }; diff --git a/web/components/dashboard/widgets/issue-panels/tabs-list.tsx b/web/components/dashboard/widgets/issue-panels/tabs-list.tsx index 306c2fdeb..d18f08f27 100644 --- a/web/components/dashboard/widgets/issue-panels/tabs-list.tsx +++ b/web/components/dashboard/widgets/issue-panels/tabs-list.tsx @@ -3,12 +3,12 @@ import { Tab } from "@headlessui/react"; // helpers import { cn } from "helpers/common.helper"; // types -import { TDurationFilterOptions, TIssuesListTypes } from "@plane/types"; +import { EDurationFilters, TIssuesListTypes } from "@plane/types"; // constants import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; type Props = { - durationFilter: TDurationFilterOptions; + durationFilter: EDurationFilters; selectedTab: TIssuesListTypes; }; @@ -48,7 +48,7 @@ export const TabsList: React.FC = observer((props) => { className={cn( "relative z-[1] font-semibold text-xs rounded-[3px] py-1.5 text-custom-text-400 focus:outline-none transition duration-500", { - "text-custom-text-100 bg-custom-background-100": selectedTab === tab.key, + "text-custom-text-100": selectedTab === tab.key, "hover:text-custom-text-300": selectedTab !== tab.key, } )} diff --git a/web/components/dashboard/widgets/issues-by-priority.tsx b/web/components/dashboard/widgets/issues-by-priority.tsx index 91e321b05..3e9823fe4 100644 --- a/web/components/dashboard/widgets/issues-by-priority.tsx +++ b/web/components/dashboard/widgets/issues-by-priority.tsx @@ -1,82 +1,36 @@ -import { useEffect, useState } from "react"; +import { useEffect } from "react"; +import { useRouter } from "next/router"; import Link from "next/link"; import { observer } from "mobx-react-lite"; // hooks import { useDashboard } from "hooks/store"; // components -import { MarimekkoGraph } from "components/ui"; import { DurationFilterDropdown, IssuesByPriorityEmptyState, WidgetLoader, WidgetProps, } from "components/dashboard/widgets"; -// ui -import { PriorityIcon } from "@plane/ui"; // helpers import { getCustomDates } from "helpers/dashboard.helper"; // types -import { TIssuesByPriorityWidgetFilters, TIssuesByPriorityWidgetResponse } from "@plane/types"; +import { EDurationFilters, TIssuesByPriorityWidgetFilters, TIssuesByPriorityWidgetResponse } from "@plane/types"; // constants -import { PRIORITY_GRAPH_GRADIENTS } from "constants/dashboard"; -import { ISSUE_PRIORITIES } from "constants/issue"; - -const TEXT_COLORS = { - urgent: "#F4A9AA", - high: "#AB4800", - medium: "#AB6400", - low: "#1F2D5C", - none: "#60646C", -}; - -const CustomBar = (props: any) => { - const { bar, workspaceSlug } = props; - // states - const [isMouseOver, setIsMouseOver] = useState(false); - - return ( - - setIsMouseOver(true)} - onMouseLeave={() => setIsMouseOver(false)} - > - - - {bar?.id} - - - - ); -}; +import { IssuesByPriorityGraph } from "components/graphs"; const WIDGET_KEY = "issues_by_priority"; export const IssuesByPriorityWidget: React.FC = observer((props) => { const { dashboardId, workspaceSlug } = props; + // router + const router = useRouter(); // store hooks const { fetchWidgetStats, getWidgetDetails, getWidgetStats, updateDashboardWidgetFilters } = useDashboard(); // derived values const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - const selectedDuration = widgetDetails?.widget_filters.duration ?? "none"; + const selectedDuration = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; + const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; const handleUpdateFilters = async (filters: Partial) => { if (!widgetDetails) return; @@ -86,7 +40,10 @@ export const IssuesByPriorityWidget: React.FC = observer((props) => filters, }); - const filterDates = getCustomDates(filters.duration ?? selectedDuration); + const filterDates = getCustomDates( + filters.duration ?? selectedDuration, + filters.custom_dates ?? selectedCustomDates + ); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), @@ -94,7 +51,7 @@ export const IssuesByPriorityWidget: React.FC = observer((props) => }; useEffect(() => { - const filterDates = getCustomDates(selectedDuration); + const filterDates = getCustomDates(selectedDuration, selectedCustomDates); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), @@ -105,31 +62,10 @@ export const IssuesByPriorityWidget: React.FC = observer((props) => if (!widgetDetails || !widgetStats) return ; const totalCount = widgetStats.reduce((acc, item) => acc + item?.count, 0); - const chartData = widgetStats - .filter((i) => i.count !== 0) - .map((item) => ({ - priority: item?.priority, - percentage: (item?.count / totalCount) * 100, - urgent: item?.priority === "urgent" ? 1 : 0, - high: item?.priority === "high" ? 1 : 0, - medium: item?.priority === "medium" ? 1 : 0, - low: item?.priority === "low" ? 1 : 0, - none: item?.priority === "none" ? 1 : 0, - })); - - const CustomBarsLayer = (props: any) => { - const { bars } = props; - - return ( - - {bars - ?.filter((b: any) => b?.value === 1) // render only bars with value 1 - .map((bar: any) => ( - - ))} - - ); - }; + const chartData = widgetStats.map((item) => ({ + priority: item?.priority, + priority_count: item?.count, + })); return (
    @@ -141,60 +77,27 @@ export const IssuesByPriorityWidget: React.FC = observer((props) => Assigned by priority + onChange={(val, customDates) => handleUpdateFilters({ duration: val, + ...(val === "custom" ? { custom_dates: customDates } : {}), }) } />
    {totalCount > 0 ? ( -
    +
    - ({ - id: p.key, - value: p.key, - }))} - axisBottom={null} - axisLeft={null} - height="119px" - margin={{ - top: 11, - right: 0, - bottom: 0, - left: 0, + onBarClick={(datum) => { + router.push( + `/${workspaceSlug}/workspace-views/assigned?priority=${`${datum.data.priority}`.toLowerCase()}` + ); }} - defs={PRIORITY_GRAPH_GRADIENTS} - fill={ISSUE_PRIORITIES.map((p) => ({ - match: { - id: p.key, - }, - id: `gradient${p.title}`, - }))} - tooltip={() => <>} - enableGridX={false} - enableGridY={false} - layers={[CustomBarsLayer]} /> -
    - {chartData.map((item) => ( -

    - - {item.percentage.toFixed(0)}% -

    - ))} -
    ) : ( diff --git a/web/components/dashboard/widgets/issues-by-state-group.tsx b/web/components/dashboard/widgets/issues-by-state-group.tsx index a0eb6c70f..b301d30f3 100644 --- a/web/components/dashboard/widgets/issues-by-state-group.tsx +++ b/web/components/dashboard/widgets/issues-by-state-group.tsx @@ -15,7 +15,12 @@ import { // helpers import { getCustomDates } from "helpers/dashboard.helper"; // types -import { TIssuesByStateGroupsWidgetFilters, TIssuesByStateGroupsWidgetResponse, TStateGroups } from "@plane/types"; +import { + EDurationFilters, + TIssuesByStateGroupsWidgetFilters, + TIssuesByStateGroupsWidgetResponse, + TStateGroups, +} from "@plane/types"; // constants import { STATE_GROUP_GRAPH_COLORS, STATE_GROUP_GRAPH_GRADIENTS } from "constants/dashboard"; import { STATE_GROUPS } from "constants/state"; @@ -34,7 +39,8 @@ export const IssuesByStateGroupWidget: React.FC = observer((props) // derived values const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - const selectedDuration = widgetDetails?.widget_filters.duration ?? "none"; + const selectedDuration = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; + const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; const handleUpdateFilters = async (filters: Partial) => { if (!widgetDetails) return; @@ -44,7 +50,10 @@ export const IssuesByStateGroupWidget: React.FC = observer((props) filters, }); - const filterDates = getCustomDates(filters.duration ?? selectedDuration); + const filterDates = getCustomDates( + filters.duration ?? selectedDuration, + filters.custom_dates ?? selectedCustomDates + ); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), @@ -53,7 +62,7 @@ export const IssuesByStateGroupWidget: React.FC = observer((props) // fetch widget stats useEffect(() => { - const filterDates = getCustomDates(selectedDuration); + const filterDates = getCustomDates(selectedDuration, selectedCustomDates); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), @@ -139,10 +148,12 @@ export const IssuesByStateGroupWidget: React.FC = observer((props) Assigned by state + onChange={(val, customDates) => handleUpdateFilters({ duration: val, + ...(val === "custom" ? { custom_dates: customDates } : {}), }) } /> diff --git a/web/components/dashboard/widgets/loaders/recent-collaborators.tsx b/web/components/dashboard/widgets/loaders/recent-collaborators.tsx index d838967af..dc2163128 100644 --- a/web/components/dashboard/widgets/loaders/recent-collaborators.tsx +++ b/web/components/dashboard/widgets/loaders/recent-collaborators.tsx @@ -2,17 +2,16 @@ import { Loader } from "@plane/ui"; export const RecentCollaboratorsWidgetLoader = () => ( - - -
    - {Array.from({ length: 8 }).map((_, index) => ( -
    + <> + {Array.from({ length: 8 }).map((_, index) => ( + +
    - ))} -
    - + + ))} + ); diff --git a/web/components/dashboard/widgets/recent-activity.tsx b/web/components/dashboard/widgets/recent-activity.tsx index fc16946d8..ec99ca771 100644 --- a/web/components/dashboard/widgets/recent-activity.tsx +++ b/web/components/dashboard/widgets/recent-activity.tsx @@ -8,11 +8,12 @@ import { useDashboard, useUser } from "hooks/store"; import { ActivityIcon, ActivityMessage, IssueLink } from "components/core"; import { RecentActivityEmptyState, WidgetLoader, WidgetProps } from "components/dashboard/widgets"; // ui -import { Avatar } from "@plane/ui"; +import { Avatar, getButtonStyling } from "@plane/ui"; // helpers import { calculateTimeAgo } from "helpers/date-time.helper"; // types import { TRecentActivityWidgetResponse } from "@plane/types"; +import { cn } from "helpers/common.helper"; const WIDGET_KEY = "recent_activity"; @@ -23,6 +24,7 @@ export const RecentActivityWidget: React.FC = observer((props) => { // derived values const { fetchWidgetStats, getWidgetStats } = useDashboard(); const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); + const redirectionLink = `/${workspaceSlug}/profile/${currentUser?.id}/activity`; useEffect(() => { fetchWidgetStats(workspaceSlug, dashboardId, { @@ -35,7 +37,7 @@ export const RecentActivityWidget: React.FC = observer((props) => { return (
    - + Your issue activities {widgetStats.length > 0 ? ( @@ -83,6 +85,15 @@ export const RecentActivityWidget: React.FC = observer((props) => {
    ))} + + View all +
    ) : (
    diff --git a/web/components/dashboard/widgets/recent-collaborators.tsx b/web/components/dashboard/widgets/recent-collaborators.tsx deleted file mode 100644 index 2fafbb9ac..000000000 --- a/web/components/dashboard/widgets/recent-collaborators.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { useEffect } from "react"; -import Link from "next/link"; -import { observer } from "mobx-react-lite"; -// hooks -import { useDashboard, useMember, useUser } from "hooks/store"; -// components -import { RecentCollaboratorsEmptyState, WidgetLoader, WidgetProps } from "components/dashboard/widgets"; -// ui -import { Avatar } from "@plane/ui"; -// types -import { TRecentCollaboratorsWidgetResponse } from "@plane/types"; - -type CollaboratorListItemProps = { - issueCount: number; - userId: string; - workspaceSlug: string; -}; - -const WIDGET_KEY = "recent_collaborators"; - -const CollaboratorListItem: React.FC = observer((props) => { - const { issueCount, userId, workspaceSlug } = props; - // store hooks - const { currentUser } = useUser(); - const { getUserDetails } = useMember(); - // derived values - const userDetails = getUserDetails(userId); - const isCurrentUser = userId === currentUser?.id; - - if (!userDetails) return null; - - return ( - -
    - -
    -
    - {isCurrentUser ? "You" : userDetails?.display_name} -
    -

    - {issueCount} active issue{issueCount > 1 ? "s" : ""} -

    - - ); -}); - -export const RecentCollaboratorsWidget: React.FC = observer((props) => { - const { dashboardId, workspaceSlug } = props; - // store hooks - const { fetchWidgetStats, getWidgetStats } = useDashboard(); - const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - - useEffect(() => { - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - if (!widgetStats) return ; - - return ( -
    -
    -

    Most active members

    -

    - Top eight active members in your project by last activity -

    -
    - {widgetStats.length > 1 ? ( -
    - {widgetStats.map((user) => ( - - ))} -
    - ) : ( -
    - -
    - )} -
    - ); -}); diff --git a/web/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx b/web/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx new file mode 100644 index 000000000..48c448075 --- /dev/null +++ b/web/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx @@ -0,0 +1,120 @@ +import { useEffect } from "react"; +import Link from "next/link"; +import { observer } from "mobx-react"; +import useSWR from "swr"; +// store hooks +import { useDashboard, useMember, useUser } from "hooks/store"; +// components +import { WidgetLoader } from "../loaders"; +// ui +import { Avatar } from "@plane/ui"; +// types +import { TRecentCollaboratorsWidgetResponse } from "@plane/types"; + +type CollaboratorListItemProps = { + issueCount: number; + userId: string; + workspaceSlug: string; +}; + +const CollaboratorListItem: React.FC = observer((props) => { + const { issueCount, userId, workspaceSlug } = props; + // store hooks + const { currentUser } = useUser(); + const { getUserDetails } = useMember(); + // derived values + const userDetails = getUserDetails(userId); + const isCurrentUser = userId === currentUser?.id; + + if (!userDetails) return null; + + return ( + +
    + +
    +
    + {isCurrentUser ? "You" : userDetails?.display_name} +
    +

    + {issueCount} active issue{issueCount > 1 ? "s" : ""} +

    + + ); +}); + +type CollaboratorsListProps = { + cursor: string; + dashboardId: string; + perPage: number; + searchQuery?: string; + updateIsLoading?: (isLoading: boolean) => void; + updateResultsCount: (count: number) => void; + updateTotalPages: (count: number) => void; + workspaceSlug: string; +}; + +const WIDGET_KEY = "recent_collaborators"; + +export const CollaboratorsList: React.FC = (props) => { + const { + cursor, + dashboardId, + perPage, + searchQuery = "", + updateIsLoading, + updateResultsCount, + updateTotalPages, + workspaceSlug, + } = props; + // store hooks + const { fetchWidgetStats } = useDashboard(); + + const { data: widgetStats } = useSWR( + workspaceSlug && dashboardId && cursor + ? `WIDGET_STATS_${workspaceSlug}_${dashboardId}_${cursor}_${searchQuery}` + : null, + workspaceSlug && dashboardId && cursor + ? () => + fetchWidgetStats(workspaceSlug, dashboardId, { + cursor, + per_page: perPage, + search: searchQuery, + widget_key: WIDGET_KEY, + }) + : null + ) as { + data: TRecentCollaboratorsWidgetResponse | undefined; + }; + + useEffect(() => { + updateIsLoading?.(true); + + if (!widgetStats) return; + + updateIsLoading?.(false); + updateTotalPages(widgetStats.total_pages); + updateResultsCount(widgetStats.results.length); + }, [updateIsLoading, updateResultsCount, updateTotalPages, widgetStats]); + + if (!widgetStats) return ; + + return ( + <> + {widgetStats?.results.map((user) => ( + + ))} + + ); +}; diff --git a/web/components/dashboard/widgets/recent-collaborators/default-list.tsx b/web/components/dashboard/widgets/recent-collaborators/default-list.tsx new file mode 100644 index 000000000..d3f857824 --- /dev/null +++ b/web/components/dashboard/widgets/recent-collaborators/default-list.tsx @@ -0,0 +1,59 @@ +import { useState } from "react"; +// components +import { CollaboratorsList } from "./collaborators-list"; +// ui +import { Button } from "@plane/ui"; + +type Props = { + dashboardId: string; + perPage: number; + workspaceSlug: string; +}; + +export const DefaultCollaboratorsList: React.FC = (props) => { + const { dashboardId, perPage, workspaceSlug } = props; + // states + const [pageCount, setPageCount] = useState(1); + const [totalPages, setTotalPages] = useState(0); + const [resultsCount, setResultsCount] = useState(0); + + const handleLoadMore = () => setPageCount((prev) => prev + 1); + + const updateTotalPages = (count: number) => setTotalPages(count); + + const updateResultsCount = (count: number) => setResultsCount(count); + + const collaboratorsPages: JSX.Element[] = []; + for (let i = 0; i < pageCount; i++) + collaboratorsPages.push( + + ); + + return ( + <> +
    + {collaboratorsPages} +
    + {pageCount < totalPages && resultsCount !== 0 && ( +
    + +
    + )} + + ); +}; diff --git a/web/components/dashboard/widgets/recent-collaborators/index.ts b/web/components/dashboard/widgets/recent-collaborators/index.ts new file mode 100644 index 000000000..1efe34c51 --- /dev/null +++ b/web/components/dashboard/widgets/recent-collaborators/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/web/components/dashboard/widgets/recent-collaborators/root.tsx b/web/components/dashboard/widgets/recent-collaborators/root.tsx new file mode 100644 index 000000000..5f611b462 --- /dev/null +++ b/web/components/dashboard/widgets/recent-collaborators/root.tsx @@ -0,0 +1,48 @@ +import { useState } from "react"; +import { Search } from "lucide-react"; +// components +import { DefaultCollaboratorsList } from "./default-list"; +import { SearchedCollaboratorsList } from "./search-list"; +8; +// types +import { WidgetProps } from "components/dashboard/widgets"; + +const PER_PAGE = 8; + +export const RecentCollaboratorsWidget: React.FC = (props) => { + const { dashboardId, workspaceSlug } = props; + // states + const [searchQuery, setSearchQuery] = useState(""); + + return ( +
    +
    +
    +

    Most active members

    +

    + Top eight active members in your project by last activity +

    +
    +
    + + setSearchQuery(e.target.value)} + /> +
    +
    + {searchQuery.trim() !== "" ? ( + + ) : ( + + )} +
    + ); +}; diff --git a/web/components/dashboard/widgets/recent-collaborators/search-list.tsx b/web/components/dashboard/widgets/recent-collaborators/search-list.tsx new file mode 100644 index 000000000..cd8826100 --- /dev/null +++ b/web/components/dashboard/widgets/recent-collaborators/search-list.tsx @@ -0,0 +1,80 @@ +import { useState } from "react"; +import Image from "next/image"; +import { useTheme } from "next-themes"; +// components +import { CollaboratorsList } from "./collaborators-list"; +// ui +import { Button } from "@plane/ui"; +// assets +import DarkImage from "public/empty-state/dashboard/dark/recent-collaborators-1.svg"; +import LightImage from "public/empty-state/dashboard/light/recent-collaborators-1.svg"; + +type Props = { + dashboardId: string; + perPage: number; + searchQuery: string; + workspaceSlug: string; +}; + +export const SearchedCollaboratorsList: React.FC = (props) => { + const { dashboardId, perPage, searchQuery, workspaceSlug } = props; + // states + const [pageCount, setPageCount] = useState(1); + const [totalPages, setTotalPages] = useState(0); + const [resultsCount, setResultsCount] = useState(0); + const [isLoading, setIsLoading] = useState(true); + // next-themes + const { resolvedTheme } = useTheme(); + + const handleLoadMore = () => setPageCount((prev) => prev + 1); + + const updateTotalPages = (count: number) => setTotalPages(count); + + const updateResultsCount = (count: number) => setResultsCount(count); + + const collaboratorsPages: JSX.Element[] = []; + for (let i = 0; i < pageCount; i++) + collaboratorsPages.push( + + ); + + const emptyStateImage = resolvedTheme === "dark" ? DarkImage : LightImage; + + return ( + <> +
    + {collaboratorsPages} +
    + {!isLoading && totalPages === 0 && ( +
    +
    + Recent collaborators +
    +

    No matching member

    +
    + )} + {pageCount < totalPages && resultsCount !== 0 && ( +
    + +
    + )} + + ); +}; diff --git a/web/components/graphs/index.ts b/web/components/graphs/index.ts new file mode 100644 index 000000000..305c3944e --- /dev/null +++ b/web/components/graphs/index.ts @@ -0,0 +1 @@ +export * from "./issues-by-priority"; diff --git a/web/components/graphs/issues-by-priority.tsx b/web/components/graphs/issues-by-priority.tsx new file mode 100644 index 000000000..0d4bf37b5 --- /dev/null +++ b/web/components/graphs/issues-by-priority.tsx @@ -0,0 +1,103 @@ +import { Theme } from "@nivo/core"; +import { ComputedDatum } from "@nivo/bar"; +// components +import { BarGraph } from "components/ui"; +// helpers +import { capitalizeFirstLetter } from "helpers/string.helper"; +// types +import { TIssuePriorities } from "@plane/types"; +// constants +import { PRIORITY_GRAPH_GRADIENTS } from "constants/dashboard"; +import { ISSUE_PRIORITIES } from "constants/issue"; + +type Props = { + borderRadius?: number; + data: { + priority: TIssuePriorities; + priority_count: number; + }[]; + height?: number; + onBarClick?: ( + datum: ComputedDatum & { + color: string; + } + ) => void; + padding?: number; + theme?: Theme; +}; + +const PRIORITY_TEXT_COLORS = { + urgent: "#CE2C31", + high: "#AB4800", + medium: "#AB6400", + low: "#1F2D5C", + none: "#60646C", +}; + +export const IssuesByPriorityGraph: React.FC = (props) => { + const { borderRadius = 8, data, height = 300, onBarClick, padding = 0.05, theme } = props; + + const chartData = data.map((priority) => ({ + priority: capitalizeFirstLetter(priority.priority), + value: priority.priority_count, + })); + + return ( + p.priority_count)} + axisBottom={{ + tickPadding: 8, + tickSize: 0, + }} + tooltip={(datum) => ( +
    + + {datum.data.priority}: + {datum.value} +
    + )} + colors={({ data }) => `url(#gradient${data.priority})`} + defs={PRIORITY_GRAPH_GRADIENTS} + fill={ISSUE_PRIORITIES.map((p) => ({ + match: { + id: p.key, + }, + id: `gradient${p.title}`, + }))} + onClick={(datum) => { + if (onBarClick) onBarClick(datum); + }} + theme={{ + axis: { + domain: { + line: { + stroke: "transparent", + }, + }, + ticks: { + text: { + fontSize: 13, + }, + }, + }, + grid: { + line: { + stroke: "transparent", + }, + }, + ...theme, + }} + /> + ); +}; diff --git a/web/components/inbox/inbox-issue-actions.tsx b/web/components/inbox/inbox-issue-actions.tsx index 200d541ab..4d4cfa0cc 100644 --- a/web/components/inbox/inbox-issue-actions.tsx +++ b/web/components/inbox/inbox-issue-actions.tsx @@ -17,7 +17,7 @@ import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Trash2, XCircle } from "lucide-react"; // types -import type { TInboxStatus, TInboxDetailedStatus } from "@plane/types"; +import type { TInboxDetailedStatus } from "@plane/types"; import { EUserProjectRoles } from "constants/project"; import { ISSUE_DELETED } from "constants/event-tracker"; @@ -29,7 +29,7 @@ type TInboxIssueActionsHeader = { }; type TInboxIssueOperations = { - updateInboxIssueStatus: (data: TInboxStatus) => Promise; + updateInboxIssueStatus: (data: TInboxDetailedStatus) => Promise; removeInboxIssue: () => Promise; }; diff --git a/web/components/profile/activity/activity-list.tsx b/web/components/profile/activity/activity-list.tsx new file mode 100644 index 000000000..066912721 --- /dev/null +++ b/web/components/profile/activity/activity-list.tsx @@ -0,0 +1,162 @@ +import Link from "next/link"; +import { observer } from "mobx-react"; +import { History, MessageSquare } from "lucide-react"; +// editor +import { RichReadOnlyEditor } from "@plane/rich-text-editor"; +// hooks +import { useUser } from "hooks/store"; +// components +import { ActivityIcon, ActivityMessage, IssueLink } from "components/core"; +// ui +import { ActivitySettingsLoader } from "components/ui"; +// helpers +import { calculateTimeAgo } from "helpers/date-time.helper"; +// types +import { IUserActivityResponse } from "@plane/types"; + +type Props = { + activity: IUserActivityResponse | undefined; +}; + +export const ActivityList: React.FC = observer((props) => { + const { activity } = props; + // store hooks + const { currentUser } = useUser(); + + // TODO: refactor this component + return ( + <> + {activity ? ( +
      + {activity.results.map((activityItem: any) => { + if (activityItem.field === "comment") + return ( +
      +
      +
      + {activityItem.field ? ( + activityItem.new_value === "restore" && + ) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? ( + {activityItem.actor_detail.display_name} + ) : ( +
      + {activityItem.actor_detail.display_name?.[0]} +
      + )} + + + +
      +
      +
      +
      + {activityItem.actor_detail.is_bot + ? activityItem.actor_detail.first_name + " Bot" + : activityItem.actor_detail.display_name} +
      +

      + Commented {calculateTimeAgo(activityItem.created_at)} +

      +
      +
      + +
      +
      +
      +
      + ); + + const message = + activityItem.verb === "created" && + !["cycles", "modules", "attachment", "link", "estimate"].includes(activityItem.field) && + !activityItem.field ? ( + + created + + ) : ( + + ); + + if ("field" in activityItem && activityItem.field !== "updated_by") + return ( +
    • +
      +
      + <> +
      +
      +
      +
      + {activityItem.field ? ( + activityItem.new_value === "restore" ? ( + + ) : ( + + ) + ) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? ( + {activityItem.actor_detail.display_name} + ) : ( +
      + {activityItem.actor_detail.display_name?.[0]} +
      + )} +
      +
      +
      +
      +
      +
      + {activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? ( + Plane + ) : activityItem.actor_detail.is_bot ? ( + {activityItem.actor_detail.first_name} Bot + ) : ( + + + {currentUser?.id === activityItem.actor_detail.id + ? "You" + : activityItem.actor_detail.display_name} + + + )}{" "} +
      + {message}{" "} + + {calculateTimeAgo(activityItem.created_at)} + +
      +
      +
      + +
      +
      +
    • + ); + })} +
    + ) : ( + + )} + + ); +}); diff --git a/web/components/profile/activity/download-button.tsx b/web/components/profile/activity/download-button.tsx new file mode 100644 index 000000000..ff928dc2a --- /dev/null +++ b/web/components/profile/activity/download-button.tsx @@ -0,0 +1,57 @@ +import { useState } from "react"; +import { useRouter } from "next/router"; +// services +import { UserService } from "services/user.service"; +// ui +import { Button } from "@plane/ui"; +// helpers +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; + +const userService = new UserService(); + +export const DownloadActivityButton = () => { + // states + const [isDownloading, setIsDownloading] = useState(false); + // router + const router = useRouter(); + const { workspaceSlug, userId } = router.query; + + const handleDownload = async () => { + const today = renderFormattedPayloadDate(new Date()); + + if (!workspaceSlug || !userId || !today) return; + + setIsDownloading(true); + + const csv = await userService + .downloadProfileActivity(workspaceSlug.toString(), userId.toString(), { + date: today, + }) + .finally(() => setIsDownloading(false)); + + // create a Blob object + const blob = new Blob([csv], { type: "text/csv" }); + + // create URL for the Blob object + const url = window.URL.createObjectURL(blob); + + // create a link element + const a = document.createElement("a"); + a.href = url; + a.download = `profile-activity-${Date.now()}.csv`; + document.body.appendChild(a); + + // simulate click on the link element to trigger download + a.click(); + + // cleanup + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + }; + + return ( + + ); +}; diff --git a/web/components/profile/activity/index.ts b/web/components/profile/activity/index.ts new file mode 100644 index 000000000..3b202d6c5 --- /dev/null +++ b/web/components/profile/activity/index.ts @@ -0,0 +1,4 @@ +export * from "./activity-list"; +export * from "./download-button"; +export * from "./profile-activity-list"; +export * from "./workspace-activity-list"; diff --git a/web/components/profile/activity/profile-activity-list.tsx b/web/components/profile/activity/profile-activity-list.tsx new file mode 100644 index 000000000..3912c8568 --- /dev/null +++ b/web/components/profile/activity/profile-activity-list.tsx @@ -0,0 +1,190 @@ +import { useEffect } from "react"; +import Link from "next/link"; +import { observer } from "mobx-react"; +import useSWR from "swr"; +import { History, MessageSquare } from "lucide-react"; +// hooks +import { useUser } from "hooks/store"; +// services +import { UserService } from "services/user.service"; +// editor +import { RichReadOnlyEditor } from "@plane/rich-text-editor"; +// components +import { ActivityIcon, ActivityMessage, IssueLink } from "components/core"; +// ui +import { ActivitySettingsLoader } from "components/ui"; +// helpers +import { calculateTimeAgo } from "helpers/date-time.helper"; +// fetch-keys +import { USER_ACTIVITY } from "constants/fetch-keys"; + +// services +const userService = new UserService(); + +type Props = { + cursor: string; + perPage: number; + updateResultsCount: (count: number) => void; + updateTotalPages: (count: number) => void; +}; + +export const ProfileActivityListPage: React.FC = observer((props) => { + const { cursor, perPage, updateResultsCount, updateTotalPages } = props; + // store hooks + const { currentUser } = useUser(); + + const { data: userProfileActivity } = useSWR( + USER_ACTIVITY({ + cursor, + }), + () => + userService.getUserActivity({ + cursor, + per_page: perPage, + }) + ); + + useEffect(() => { + if (!userProfileActivity) return; + + updateTotalPages(userProfileActivity.total_pages); + updateResultsCount(userProfileActivity.results.length); + }, [updateResultsCount, updateTotalPages, userProfileActivity]); + + // TODO: refactor this component + return ( + <> + {userProfileActivity ? ( +
      + {userProfileActivity.results.map((activityItem: any) => { + if (activityItem.field === "comment") + return ( +
      +
      +
      + {activityItem.field ? ( + activityItem.new_value === "restore" && + ) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? ( + {activityItem.actor_detail.display_name} + ) : ( +
      + {activityItem.actor_detail.display_name?.[0]} +
      + )} + + + +
      +
      +
      +
      + {activityItem.actor_detail.is_bot + ? activityItem.actor_detail.first_name + " Bot" + : activityItem.actor_detail.display_name} +
      +

      + Commented {calculateTimeAgo(activityItem.created_at)} +

      +
      +
      + +
      +
      +
      +
      + ); + + const message = + activityItem.verb === "created" && + !["cycles", "modules", "attachment", "link", "estimate"].includes(activityItem.field) && + !activityItem.field ? ( + + created + + ) : ( + + ); + + if ("field" in activityItem && activityItem.field !== "updated_by") + return ( +
    • +
      +
      + <> +
      +
      +
      +
      + {activityItem.field ? ( + activityItem.new_value === "restore" ? ( + + ) : ( + + ) + ) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? ( + {activityItem.actor_detail.display_name} + ) : ( +
      + {activityItem.actor_detail.display_name?.[0]} +
      + )} +
      +
      +
      +
      +
      +
      + {activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? ( + Plane + ) : activityItem.actor_detail.is_bot ? ( + {activityItem.actor_detail.first_name} Bot + ) : ( + + + {currentUser?.id === activityItem.actor_detail.id + ? "You" + : activityItem.actor_detail.display_name} + + + )}{" "} +
      + {message}{" "} + + {calculateTimeAgo(activityItem.created_at)} + +
      +
      +
      + +
      +
      +
    • + ); + })} +
    + ) : ( + + )} + + ); +}); diff --git a/web/components/profile/activity/workspace-activity-list.tsx b/web/components/profile/activity/workspace-activity-list.tsx new file mode 100644 index 000000000..c2c75a195 --- /dev/null +++ b/web/components/profile/activity/workspace-activity-list.tsx @@ -0,0 +1,50 @@ +import { useEffect } from "react"; +import { useRouter } from "next/router"; +import useSWR from "swr"; +// services +import { UserService } from "services/user.service"; +// components +import { ActivityList } from "./activity-list"; +// fetch-keys +import { USER_PROFILE_ACTIVITY } from "constants/fetch-keys"; + +// services +const userService = new UserService(); + +type Props = { + cursor: string; + perPage: number; + updateResultsCount: (count: number) => void; + updateTotalPages: (count: number) => void; +}; + +export const WorkspaceActivityListPage: React.FC = (props) => { + const { cursor, perPage, updateResultsCount, updateTotalPages } = props; + // router + const router = useRouter(); + const { workspaceSlug, userId } = router.query; + + const { data: userProfileActivity } = useSWR( + workspaceSlug && userId + ? USER_PROFILE_ACTIVITY(workspaceSlug.toString(), userId.toString(), { + cursor, + }) + : null, + workspaceSlug && userId + ? () => + userService.getUserProfileActivity(workspaceSlug.toString(), userId.toString(), { + cursor, + per_page: perPage, + }) + : null + ); + + useEffect(() => { + if (!userProfileActivity) return; + + updateTotalPages(userProfileActivity.total_pages); + updateResultsCount(userProfileActivity.results.length); + }, [updateResultsCount, updateTotalPages, userProfileActivity]); + + return ; +}; diff --git a/web/components/profile/index.ts b/web/components/profile/index.ts index f6d2a3775..35ac288ad 100644 --- a/web/components/profile/index.ts +++ b/web/components/profile/index.ts @@ -1,3 +1,4 @@ +export * from "./activity"; export * from "./overview"; export * from "./navbar"; export * from "./profile-issues-filter"; diff --git a/web/components/profile/navbar.tsx b/web/components/profile/navbar.tsx index 4361b7a9d..582f0f26b 100644 --- a/web/components/profile/navbar.tsx +++ b/web/components/profile/navbar.tsx @@ -27,10 +27,11 @@ export const ProfileNavbar: React.FC = (props) => { {tabsList.map((tab) => ( {tab.label} diff --git a/web/components/profile/overview/activity.tsx b/web/components/profile/overview/activity.tsx index 58bbb6898..112c073ab 100644 --- a/web/components/profile/overview/activity.tsx +++ b/web/components/profile/overview/activity.tsx @@ -27,15 +27,18 @@ export const ProfileActivity = observer(() => { const { currentUser } = useUser(); const { data: userProfileActivity } = useSWR( - workspaceSlug && userId ? USER_PROFILE_ACTIVITY(workspaceSlug.toString(), userId.toString()) : null, + workspaceSlug && userId ? USER_PROFILE_ACTIVITY(workspaceSlug.toString(), userId.toString(), {}) : null, workspaceSlug && userId - ? () => userService.getUserProfileActivity(workspaceSlug.toString(), userId.toString()) + ? () => + userService.getUserProfileActivity(workspaceSlug.toString(), userId.toString(), { + per_page: 10, + }) : null ); return (
    -

    Recent Activity

    +

    Recent activity

    {userProfileActivity ? ( userProfileActivity.results.length > 0 ? ( diff --git a/web/components/profile/overview/priority-distribution.tsx b/web/components/profile/overview/priority-distribution.tsx deleted file mode 100644 index 8a931183f..000000000 --- a/web/components/profile/overview/priority-distribution.tsx +++ /dev/null @@ -1,88 +0,0 @@ -// ui -import { BarGraph, ProfileEmptyState } from "components/ui"; -import { Loader } from "@plane/ui"; -// image -import emptyBarGraph from "public/empty-state/empty_bar_graph.svg"; -// helpers -import { capitalizeFirstLetter } from "helpers/string.helper"; -// types -import { IUserProfileData } from "@plane/types"; - -type Props = { - userProfile: IUserProfileData | undefined; -}; - -export const ProfilePriorityDistribution: React.FC = ({ userProfile }) => ( -
    -

    Issues by Priority

    - {userProfile ? ( -
    - {userProfile.priority_distribution.length > 0 ? ( - ({ - priority: capitalizeFirstLetter(priority.priority ?? "None"), - value: priority.priority_count, - }))} - height="300px" - indexBy="priority" - keys={["value"]} - borderRadius={4} - padding={0.7} - customYAxisTickValues={userProfile.priority_distribution.map((p) => p.priority_count)} - tooltip={(datum) => ( -
    - - {datum.data.priority}: - {datum.value} -
    - )} - colors={(datum) => { - if (datum.data.priority === "Urgent") return "#991b1b"; - else if (datum.data.priority === "High") return "#ef4444"; - else if (datum.data.priority === "Medium") return "#f59e0b"; - else if (datum.data.priority === "Low") return "#16a34a"; - else return "#e5e5e5"; - }} - theme={{ - axis: { - domain: { - line: { - stroke: "transparent", - }, - }, - }, - grid: { - line: { - stroke: "transparent", - }, - }, - }} - /> - ) : ( -
    - -
    - )} -
    - ) : ( -
    - - - - - - - -
    - )} -
    -); diff --git a/web/components/profile/overview/priority-distribution/index.ts b/web/components/profile/overview/priority-distribution/index.ts new file mode 100644 index 000000000..64d81eb12 --- /dev/null +++ b/web/components/profile/overview/priority-distribution/index.ts @@ -0,0 +1 @@ +export * from "./priority-distribution"; diff --git a/web/components/profile/overview/priority-distribution/main-content.tsx b/web/components/profile/overview/priority-distribution/main-content.tsx new file mode 100644 index 000000000..8606f44b1 --- /dev/null +++ b/web/components/profile/overview/priority-distribution/main-content.tsx @@ -0,0 +1,31 @@ +// components +import { IssuesByPriorityGraph } from "components/graphs"; +import { ProfileEmptyState } from "components/ui"; +// assets +import emptyBarGraph from "public/empty-state/empty_bar_graph.svg"; +// types +import { IUserPriorityDistribution } from "@plane/types"; + +type Props = { + priorityDistribution: IUserPriorityDistribution[]; +}; + +export const PriorityDistributionContent: React.FC = (props) => { + const { priorityDistribution } = props; + + return ( +
    + {priorityDistribution.length > 0 ? ( + + ) : ( +
    + +
    + )} +
    + ); +}; diff --git a/web/components/profile/overview/priority-distribution/priority-distribution.tsx b/web/components/profile/overview/priority-distribution/priority-distribution.tsx new file mode 100644 index 000000000..63559bdee --- /dev/null +++ b/web/components/profile/overview/priority-distribution/priority-distribution.tsx @@ -0,0 +1,33 @@ +// components +import { PriorityDistributionContent } from "./main-content"; +// ui +import { Loader } from "@plane/ui"; +// types +import { IUserPriorityDistribution } from "@plane/types"; + +type Props = { + priorityDistribution: IUserPriorityDistribution[] | undefined; +}; + +export const ProfilePriorityDistribution: React.FC = (props) => { + const { priorityDistribution } = props; + + return ( +
    +

    Issues by priority

    + {priorityDistribution ? ( + + ) : ( +
    + + + + + + + +
    + )} +
    + ); +}; diff --git a/web/components/profile/overview/state-distribution.tsx b/web/components/profile/overview/state-distribution.tsx index 5664637e9..f38283aa7 100644 --- a/web/components/profile/overview/state-distribution.tsx +++ b/web/components/profile/overview/state-distribution.tsx @@ -17,7 +17,7 @@ export const ProfileStateDistribution: React.FC = ({ stateDistribution, u return (
    -

    Issues by State

    +

    Issues by state

    {userProfile.state_distribution.length > 0 ? (
    @@ -65,7 +65,7 @@ export const ProfileStateDistribution: React.FC = ({ stateDistribution, u backgroundColor: STATE_GROUPS[group.state_group].color, }} /> -
    {group.state_group}
    +
    {STATE_GROUPS[group.state_group].label}
    {group.state_count}
    diff --git a/web/components/profile/overview/workload.tsx b/web/components/profile/overview/workload.tsx index c091a94f7..86989748d 100644 --- a/web/components/profile/overview/workload.tsx +++ b/web/components/profile/overview/workload.tsx @@ -21,12 +21,12 @@ export const ProfileWorkload: React.FC = ({ stateDistribution }) => ( }} />
    -

    +

    {group.state_group === "unstarted" - ? "Not Started" + ? "Not started" : group.state_group === "started" ? "Working on" - : group.state_group} + : STATE_GROUPS[group.state_group].label}

    {group.state_count}

    diff --git a/web/components/profile/sidebar.tsx b/web/components/profile/sidebar.tsx index 71d935d3c..48bb7d323 100644 --- a/web/components/profile/sidebar.tsx +++ b/web/components/profile/sidebar.tsx @@ -1,9 +1,11 @@ +import { useEffect, useRef } from "react"; import { useRouter } from "next/router"; import Link from "next/link"; import useSWR from "swr"; import { Disclosure, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; // hooks +import useOutsideClickDetector from "hooks/use-outside-click-detector"; import { useApplication, useUser } from "hooks/store"; // services import { UserService } from "services/user.service"; @@ -18,8 +20,6 @@ import { renderFormattedDate } from "helpers/date-time.helper"; import { renderEmoji } from "helpers/emoji.helper"; // fetch-keys import { USER_PROFILE_PROJECT_SEGREGATION } from "constants/fetch-keys"; -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -import { useEffect, useRef } from "react"; // services const userService = new UserService(); diff --git a/web/components/ui/graphs/index.ts b/web/components/ui/graphs/index.ts index 1f40adbff..984bb642c 100644 --- a/web/components/ui/graphs/index.ts +++ b/web/components/ui/graphs/index.ts @@ -1,6 +1,5 @@ export * from "./bar-graph"; export * from "./calendar-graph"; export * from "./line-graph"; -export * from "./marimekko-graph"; export * from "./pie-graph"; export * from "./scatter-plot-graph"; diff --git a/web/components/ui/graphs/marimekko-graph.tsx b/web/components/ui/graphs/marimekko-graph.tsx deleted file mode 100644 index fd460d11b..000000000 --- a/web/components/ui/graphs/marimekko-graph.tsx +++ /dev/null @@ -1,48 +0,0 @@ -// nivo -import { ResponsiveMarimekko, SvgProps } from "@nivo/marimekko"; -// helpers -import { generateYAxisTickValues } from "helpers/graph.helper"; -// types -import { TGraph } from "./types"; -// constants -import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; - -type Props = { - id: string; - value: string; - customYAxisTickValues?: number[]; -}; - -export const MarimekkoGraph: React.FC, "height" | "width">> = ({ - id, - value, - customYAxisTickValues, - height = "400px", - width = "100%", - margin, - theme, - ...rest -}) => ( -
    - 7 ? -45 : 0, - }} - labelTextColor={{ from: "color", modifiers: [["darker", 1.6]] }} - theme={{ ...CHARTS_THEME, ...(theme ?? {}) }} - animate - {...rest} - /> -
    -); diff --git a/web/constants/dashboard.ts b/web/constants/dashboard.ts index b1cfa51d7..6ac4e7817 100644 --- a/web/constants/dashboard.ts +++ b/web/constants/dashboard.ts @@ -7,7 +7,7 @@ import OverdueIssuesLight from "public/empty-state/dashboard/light/overdue-issue import CompletedIssuesDark from "public/empty-state/dashboard/dark/completed-issues.svg"; import CompletedIssuesLight from "public/empty-state/dashboard/light/completed-issues.svg"; // types -import { TDurationFilterOptions, TIssuesListTypes, TStateGroups } from "@plane/types"; +import { EDurationFilters, TIssuesListTypes, TStateGroups } from "@plane/types"; import { Props } from "components/icons/types"; // constants import { EUserWorkspaceRoles } from "./workspace"; @@ -118,29 +118,33 @@ export const STATE_GROUP_GRAPH_COLORS: Record = { // filter duration options export const DURATION_FILTER_OPTIONS: { - key: TDurationFilterOptions; + key: EDurationFilters; label: string; }[] = [ { - key: "none", + key: EDurationFilters.NONE, label: "None", }, { - key: "today", + key: EDurationFilters.TODAY, label: "Due today", }, { - key: "this_week", - label: " Due this week", + key: EDurationFilters.THIS_WEEK, + label: "Due this week", }, { - key: "this_month", + key: EDurationFilters.THIS_MONTH, label: "Due this month", }, { - key: "this_year", + key: EDurationFilters.THIS_YEAR, label: "Due this year", }, + { + key: EDurationFilters.CUSTOM, + label: "Custom", + }, ]; // random background colors for project cards diff --git a/web/constants/fetch-keys.ts b/web/constants/fetch-keys.ts index 86386e968..3b2e97c38 100644 --- a/web/constants/fetch-keys.ts +++ b/web/constants/fetch-keys.ts @@ -164,7 +164,7 @@ export const USER_ISSUES = (workspaceSlug: string, params: any) => { return `USER_ISSUES_${workspaceSlug.toUpperCase()}_${paramsKey}`; }; -export const USER_ACTIVITY = "USER_ACTIVITY"; +export const USER_ACTIVITY = (params: { cursor?: string }) => `USER_ACTIVITY_${params?.cursor}`; export const USER_WORKSPACE_DASHBOARD = (workspaceSlug: string) => `USER_WORKSPACE_DASHBOARD_${workspaceSlug.toUpperCase()}`; export const USER_PROJECT_VIEW = (projectId: string) => `USER_PROJECT_VIEW_${projectId.toUpperCase()}`; @@ -284,8 +284,13 @@ export const getPaginatedNotificationKey = (index: number, prevData: any, worksp // profile export const USER_PROFILE_DATA = (workspaceSlug: string, userId: string) => `USER_PROFILE_ACTIVITY_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}`; -export const USER_PROFILE_ACTIVITY = (workspaceSlug: string, userId: string) => - `USER_WORKSPACE_PROFILE_ACTIVITY_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}`; +export const USER_PROFILE_ACTIVITY = ( + workspaceSlug: string, + userId: string, + params: { + cursor?: string; + } +) => `USER_WORKSPACE_PROFILE_ACTIVITY_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}_${params?.cursor}`; export const USER_PROFILE_PROJECT_SEGREGATION = (workspaceSlug: string, userId: string) => `USER_PROFILE_PROJECT_SEGREGATION_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}`; export const USER_PROFILE_ISSUES = (workspaceSlug: string, userId: string, params: any) => { diff --git a/web/constants/profile.ts b/web/constants/profile.ts index 463fd27ee..4d8e37640 100644 --- a/web/constants/profile.ts +++ b/web/constants/profile.ts @@ -63,4 +63,9 @@ export const PROFILE_ADMINS_TAB = [ label: "Subscribed", selected: "/[workspaceSlug]/profile/[userId]/subscribed", }, + { + route: "activity", + label: "Activity", + selected: "/[workspaceSlug]/profile/[userId]/activity", + }, ]; diff --git a/web/helpers/dashboard.helper.ts b/web/helpers/dashboard.helper.ts index 90319a90b..a61ec7f78 100644 --- a/web/helpers/dashboard.helper.ts +++ b/web/helpers/dashboard.helper.ts @@ -1,36 +1,40 @@ import { endOfMonth, endOfWeek, endOfYear, startOfMonth, startOfWeek, startOfYear } from "date-fns"; // helpers -import { renderFormattedPayloadDate } from "./date-time.helper"; +import { renderFormattedDate, renderFormattedPayloadDate } from "./date-time.helper"; // types -import { TDurationFilterOptions, TIssuesListTypes } from "@plane/types"; +import { EDurationFilters, TIssuesListTypes } from "@plane/types"; +// constants +import { DURATION_FILTER_OPTIONS } from "constants/dashboard"; /** * @description returns date range based on the duration filter * @param duration */ -export const getCustomDates = (duration: TDurationFilterOptions): string => { +export const getCustomDates = (duration: EDurationFilters, customDates: string[]): string => { const today = new Date(); let firstDay, lastDay; switch (duration) { - case "none": + case EDurationFilters.NONE: return ""; - case "today": + case EDurationFilters.TODAY: firstDay = renderFormattedPayloadDate(today); lastDay = renderFormattedPayloadDate(today); return `${firstDay};after,${lastDay};before`; - case "this_week": + case EDurationFilters.THIS_WEEK: firstDay = renderFormattedPayloadDate(startOfWeek(today)); lastDay = renderFormattedPayloadDate(endOfWeek(today)); return `${firstDay};after,${lastDay};before`; - case "this_month": + case EDurationFilters.THIS_MONTH: firstDay = renderFormattedPayloadDate(startOfMonth(today)); lastDay = renderFormattedPayloadDate(endOfMonth(today)); return `${firstDay};after,${lastDay};before`; - case "this_year": + case EDurationFilters.THIS_YEAR: firstDay = renderFormattedPayloadDate(startOfYear(today)); lastDay = renderFormattedPayloadDate(endOfYear(today)); return `${firstDay};after,${lastDay};before`; + case EDurationFilters.CUSTOM: + return customDates.join(","); } }; @@ -58,7 +62,7 @@ export const getRedirectionFilters = (type: TIssuesListTypes): string => { * @param duration * @param tab */ -export const getTabKey = (duration: TDurationFilterOptions, tab: TIssuesListTypes | undefined): TIssuesListTypes => { +export const getTabKey = (duration: EDurationFilters, tab: TIssuesListTypes | undefined): TIssuesListTypes => { if (!tab) return "completed"; if (tab === "completed") return tab; @@ -69,3 +73,21 @@ export const getTabKey = (duration: TDurationFilterOptions, tab: TIssuesListType else return "upcoming"; } }; + +/** + * @description returns the label for the duration filter dropdown + * @param duration + * @param customDates + */ +export const getDurationFilterDropdownLabel = (duration: EDurationFilters, customDates: string[]): string => { + if (duration !== "custom") return DURATION_FILTER_OPTIONS.find((option) => option.key === duration)?.label ?? ""; + else { + const afterDate = customDates.find((date) => date.includes("after"))?.split(";")[0]; + const beforeDate = customDates.find((date) => date.includes("before"))?.split(";")[0]; + + if (afterDate && beforeDate) return `${renderFormattedDate(afterDate)} - ${renderFormattedDate(beforeDate)}`; + else if (afterDate) return `After ${renderFormattedDate(afterDate)}`; + else if (beforeDate) return `Before ${renderFormattedDate(beforeDate)}`; + else return ""; + } +}; diff --git a/web/layouts/user-profile-layout/layout.tsx b/web/layouts/user-profile-layout/layout.tsx index 60c17d8d4..52bfc6fbf 100644 --- a/web/layouts/user-profile-layout/layout.tsx +++ b/web/layouts/user-profile-layout/layout.tsx @@ -4,6 +4,8 @@ import { observer } from "mobx-react-lite"; import { useUser } from "hooks/store"; // components import { ProfileNavbar, ProfileSidebar } from "components/profile"; +// constants +import { EUserWorkspaceRoles } from "constants/workspace"; type Props = { children: React.ReactNode; @@ -11,27 +13,25 @@ type Props = { showProfileIssuesFilter?: boolean; }; -const AUTHORIZED_ROLES = [20, 15, 10]; +const AUTHORIZED_ROLES = [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.VIEWER]; export const ProfileAuthWrapper: React.FC = observer((props) => { const { children, className, showProfileIssuesFilter } = props; + // router const router = useRouter(); - + // store hooks const { membership: { currentWorkspaceRole }, } = useUser(); - - if (!currentWorkspaceRole) return null; - - const isAuthorized = AUTHORIZED_ROLES.includes(currentWorkspaceRole); - + // derived values + const isAuthorized = currentWorkspaceRole && AUTHORIZED_ROLES.includes(currentWorkspaceRole); const isAuthorizedPath = router.pathname.includes("assigned" || "created" || "subscribed"); return (
    - + {isAuthorized || !isAuthorizedPath ? (
    {children}
    ) : ( diff --git a/web/package.json b/web/package.json index af28cbb3d..fbec571ef 100644 --- a/web/package.json +++ b/web/package.json @@ -20,7 +20,6 @@ "@nivo/core": "0.80.0", "@nivo/legends": "0.80.0", "@nivo/line": "0.80.0", - "@nivo/marimekko": "0.80.0", "@nivo/pie": "0.80.0", "@nivo/scatterplot": "0.80.0", "@plane/document-editor": "*", diff --git a/web/pages/[workspaceSlug]/profile/[userId]/activity.tsx b/web/pages/[workspaceSlug]/profile/[userId]/activity.tsx new file mode 100644 index 000000000..09269676a --- /dev/null +++ b/web/pages/[workspaceSlug]/profile/[userId]/activity.tsx @@ -0,0 +1,84 @@ +import { ReactElement, useState } from "react"; +import { useRouter } from "next/router"; +import { observer } from "mobx-react"; +// hooks +import { useUser } from "hooks/store"; +// layouts +import { AppLayout } from "layouts/app-layout"; +import { ProfileAuthWrapper } from "layouts/user-profile-layout"; +// components +import { UserProfileHeader } from "components/headers"; +import { DownloadActivityButton, WorkspaceActivityListPage } from "components/profile"; +// ui +import { Button } from "@plane/ui"; +// types +import { NextPageWithLayout } from "lib/types"; +// constants +import { EUserWorkspaceRoles } from "constants/workspace"; + +const PER_PAGE = 100; + +const ProfileActivityPage: NextPageWithLayout = observer(() => { + // states + const [pageCount, setPageCount] = useState(1); + const [totalPages, setTotalPages] = useState(0); + const [resultsCount, setResultsCount] = useState(0); + // router + const router = useRouter(); + const { userId } = router.query; + // store hooks + const { + currentUser, + membership: { currentWorkspaceRole }, + } = useUser(); + + const updateTotalPages = (count: number) => setTotalPages(count); + + const updateResultsCount = (count: number) => setResultsCount(count); + + const handleLoadMore = () => setPageCount((prev) => prev + 1); + + const activityPages: JSX.Element[] = []; + for (let i = 0; i < pageCount; i++) + activityPages.push( + + ); + + const canDownloadActivity = + currentUser?.id === userId && !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; + + return ( +
    +
    +

    Recent activity

    + {canDownloadActivity && } +
    +
    + {activityPages} + {pageCount < totalPages && resultsCount !== 0 && ( +
    + +
    + )} +
    +
    + ); +}); + +ProfileActivityPage.getLayout = function getLayout(page: ReactElement) { + return ( + }> + {page} + + ); +}; + +export default ProfileActivityPage; diff --git a/web/pages/[workspaceSlug]/profile/[userId]/index.tsx b/web/pages/[workspaceSlug]/profile/[userId]/index.tsx index 7d24a8b11..6e8a10b50 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/index.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/index.tsx @@ -49,7 +49,7 @@ const ProfileOverviewPage: NextPageWithLayout = () => {
    - +
    diff --git a/web/pages/profile/activity.tsx b/web/pages/profile/activity.tsx index 3ea65ed24..b0e8bb1a0 100644 --- a/web/pages/profile/activity.tsx +++ b/web/pages/profile/activity.tsx @@ -1,191 +1,64 @@ -import { ReactElement } from "react"; -import useSWR from "swr"; -import Link from "next/link"; +import { ReactElement, useState } from "react"; import { observer } from "mobx-react"; //hooks -import { useApplication, useUser } from "hooks/store"; -// services -import { UserService } from "services/user.service"; +import { useApplication } from "hooks/store"; // layouts import { ProfileSettingsLayout } from "layouts/settings-layout"; // components -import { ActivityIcon, ActivityMessage, IssueLink, PageHead } from "components/core"; -import { RichReadOnlyEditor } from "@plane/rich-text-editor"; -// icons -import { History, MessageSquare } from "lucide-react"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { ProfileActivityListPage } from "components/profile"; +import { PageHead } from "components/core"; // ui -import { ActivitySettingsLoader } from "components/ui"; -// fetch-keys -import { USER_ACTIVITY } from "constants/fetch-keys"; -// helper -import { calculateTimeAgo } from "helpers/date-time.helper"; +import { Button } from "@plane/ui"; // type import { NextPageWithLayout } from "lib/types"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -const userService = new UserService(); +const PER_PAGE = 100; const ProfileActivityPage: NextPageWithLayout = observer(() => { - const { data: userActivity } = useSWR(USER_ACTIVITY, () => userService.getUserActivity()); + // states + const [pageCount, setPageCount] = useState(1); + const [totalPages, setTotalPages] = useState(0); + const [resultsCount, setResultsCount] = useState(0); // store hooks - const { currentUser } = useUser(); const { theme: themeStore } = useApplication(); + const updateTotalPages = (count: number) => setTotalPages(count); + + const updateResultsCount = (count: number) => setResultsCount(count); + + const handleLoadMore = () => setPageCount((prev) => prev + 1); + + const activityPages: JSX.Element[] = []; + for (let i = 0; i < pageCount; i++) + activityPages.push( + + ); + return ( <> -
    +
    themeStore.toggleSidebar()} />

    Activity

    - {userActivity ? ( -
    -
      - {userActivity.results.map((activityItem: any) => { - if (activityItem.field === "comment") { - return ( -
      -
      -
      - {activityItem.field ? ( - activityItem.new_value === "restore" && ( - - ) - ) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? ( - {activityItem.actor_detail.display_name} - ) : ( -
      - {activityItem.actor_detail.display_name?.charAt(0)} -
      - )} - - - -
      -
      -
      -
      - {activityItem.actor_detail.is_bot - ? activityItem.actor_detail.first_name + " Bot" - : activityItem.actor_detail.display_name} -
      -

      - Commented {calculateTimeAgo(activityItem.created_at)} -

      -
      -
      - -
      -
      -
      -
      - ); - } - - const message = - activityItem.verb === "created" && - activityItem.field !== "cycles" && - activityItem.field !== "modules" && - activityItem.field !== "attachment" && - activityItem.field !== "link" && - activityItem.field !== "estimate" && - !activityItem.field ? ( - - created - - ) : ( - - ); - - if ("field" in activityItem && activityItem.field !== "updated_by") { - return ( -
    • -
      -
      - <> -
      -
      -
      -
      - {activityItem.field ? ( - activityItem.new_value === "restore" ? ( - - ) : ( - - ) - ) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? ( - {activityItem.actor_detail.display_name} - ) : ( -
      - {activityItem.actor_detail.display_name?.charAt(0)} -
      - )} -
      -
      -
      -
      -
      -
      - {activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? ( - Plane - ) : activityItem.actor_detail.is_bot ? ( - - {activityItem.actor_detail.first_name} Bot - - ) : ( - - - {currentUser?.id === activityItem.actor_detail.id - ? "You" - : activityItem.actor_detail.display_name} - - - )}{" "} -
      - {message}{" "} - - {calculateTimeAgo(activityItem.created_at)} - -
      -
      -
      - -
      -
      -
    • - ); - } - })} -
    -
    - ) : ( - - )} +
    + {activityPages} + {pageCount < totalPages && resultsCount !== 0 && ( +
    + +
    + )} +
    ); diff --git a/web/services/user.service.ts b/web/services/user.service.ts index 13ffa9c51..41111db98 100644 --- a/web/services/user.service.ts +++ b/web/services/user.service.ts @@ -9,7 +9,6 @@ import type { IUserProfileData, IUserProfileProjectSegregation, IUserSettings, - IUserWorkspaceDashboard, IUserEmailNotificationSettings, } from "@plane/types"; // helpers @@ -113,20 +112,8 @@ export class UserService extends APIService { }); } - async getUserActivity(): Promise { - return this.get(`/api/users/me/activities/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async userWorkspaceDashboard(workspaceSlug: string, month: number): Promise { - return this.get(`/api/users/me/workspaces/${workspaceSlug}/dashboard/`, { - params: { - month: month, - }, - }) + async getUserActivity(params: { per_page: number; cursor?: string }): Promise { + return this.get("/api/users/me/activities/", { params }) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -160,8 +147,31 @@ export class UserService extends APIService { }); } - async getUserProfileActivity(workspaceSlug: string, userId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/user-activity/${userId}/?per_page=15`) + async getUserProfileActivity( + workspaceSlug: string, + userId: string, + params: { + per_page: number; + cursor?: string; + } + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/user-activity/${userId}/`, { + params, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async downloadProfileActivity( + workspaceSlug: string, + userId: string, + data: { + date: string; + } + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/user-activity/${userId}/export/`, data) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; diff --git a/web/store/user/index.ts b/web/store/user/index.ts index 15f9e5772..ada2e6be7 100644 --- a/web/store/user/index.ts +++ b/web/store/user/index.ts @@ -22,7 +22,6 @@ export interface IUserRootStore { fetchCurrentUser: () => Promise; fetchCurrentUserInstanceAdminStatus: () => Promise; fetchCurrentUserSettings: () => Promise; - fetchUserDashboardInfo: (workspaceSlug: string, month: number) => Promise; // crud actions updateUserOnBoard: () => Promise; updateTourCompleted: () => Promise; @@ -68,7 +67,6 @@ export class UserRootStore implements IUserRootStore { fetchCurrentUser: action, fetchCurrentUserInstanceAdminStatus: action, fetchCurrentUserSettings: action, - fetchUserDashboardInfo: action, updateUserOnBoard: action, updateTourCompleted: action, updateCurrentUser: action, @@ -130,22 +128,6 @@ export class UserRootStore implements IUserRootStore { return response; }); - /** - * Fetches the current user dashboard info - * @returns Promise - */ - fetchUserDashboardInfo = async (workspaceSlug: string, month: number) => { - try { - const response = await this.userService.userWorkspaceDashboard(workspaceSlug, month); - runInAction(() => { - this.dashboardInfo = response; - }); - return response; - } catch (error) { - throw error; - } - }; - /** * Updates the user onboarding status * @returns Promise diff --git a/yarn.lock b/yarn.lock index 81e6224e8..0a21fcee2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1766,19 +1766,6 @@ "@react-spring/web" "9.4.5" d3-shape "^1.3.5" -"@nivo/marimekko@0.80.0": - version "0.80.0" - resolved "https://registry.yarnpkg.com/@nivo/marimekko/-/marimekko-0.80.0.tgz#1eda4207935b3776bd1d7d729dc39a0e42bb1b86" - integrity sha512-0u20SryNtbOQhtvhZsbxmgnF7o8Yc2rjDQ/gCYPTXtxeooWCuhSaRZbDCnCeyKQY3B62D7z2mu4Js4KlTEftjA== - dependencies: - "@nivo/axes" "0.80.0" - "@nivo/colors" "0.80.0" - "@nivo/legends" "0.80.0" - "@nivo/scales" "0.80.0" - "@react-spring/web" "9.4.5" - d3-shape "^1.3.5" - lodash "^4.17.21" - "@nivo/pie@0.80.0": version "0.80.0" resolved "https://registry.yarnpkg.com/@nivo/pie/-/pie-0.80.0.tgz#04b35839bf5a2b661fa4e5b677ae76b3c028471e" From 4572b7378df0e4e5ad1246cb5f8cdb39bbbfde51 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:24:58 +0530 Subject: [PATCH 035/308] [WEB-621] chore: 404 page not found (#3859) * chore: custom 404 error * chore: moved from middleware to view --- apiserver/plane/app/views/__init__.py | 4 +++- apiserver/plane/app/views/error_404.py | 5 +++++ apiserver/plane/urls.py | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 apiserver/plane/app/views/error_404.py diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 6af60ff9c..910ea006d 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -187,4 +187,6 @@ from .webhook import ( from .dashboard import ( DashboardEndpoint, WidgetsEndpoint -) \ No newline at end of file +) + +from .error_404 import custom_404_view diff --git a/apiserver/plane/app/views/error_404.py b/apiserver/plane/app/views/error_404.py new file mode 100644 index 000000000..3c31474e0 --- /dev/null +++ b/apiserver/plane/app/views/error_404.py @@ -0,0 +1,5 @@ +# views.py +from django.http import JsonResponse + +def custom_404_view(request, exception=None): + return JsonResponse({"error": "Page not found."}, status=404) diff --git a/apiserver/plane/urls.py b/apiserver/plane/urls.py index 669f3ea73..3b042ea1f 100644 --- a/apiserver/plane/urls.py +++ b/apiserver/plane/urls.py @@ -7,6 +7,7 @@ from django.views.generic import TemplateView from django.conf import settings +handler404 = "plane.app.views.error_404.custom_404_view" urlpatterns = [ path("", TemplateView.as_view(template_name="index.html")), From dbdd14493b05ae502380762d50a53369b124fd34 Mon Sep 17 00:00:00 2001 From: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:25:15 +0530 Subject: [PATCH 036/308] [WEB-662] chore: posthog debugging based on env variable (#3860) * chore: posthog debugging based on env variable * updated turbo.json * chore: true to 1 --------- Co-authored-by: gurusainath --- turbo.json | 1 + web/lib/posthog-provider.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/turbo.json b/turbo.json index bd5ee34b5..9302a7183 100644 --- a/turbo.json +++ b/turbo.json @@ -16,6 +16,7 @@ "NEXT_PUBLIC_DEPLOY_WITH_NGINX", "NEXT_PUBLIC_POSTHOG_KEY", "NEXT_PUBLIC_POSTHOG_HOST", + "NEXT_PUBLIC_POSTHOG_DEBUG", "JITSU_TRACKER_ACCESS_KEY", "JITSU_TRACKER_HOST" ], diff --git a/web/lib/posthog-provider.tsx b/web/lib/posthog-provider.tsx index e8c1b7899..c5acd2957 100644 --- a/web/lib/posthog-provider.tsx +++ b/web/lib/posthog-provider.tsx @@ -45,6 +45,7 @@ const PostHogProvider: FC = (props) => { if (posthogAPIKey && posthogHost) { posthog.init(posthogAPIKey, { api_host: posthogHost || "https://app.posthog.com", + debug: process.env.NEXT_PUBLIC_POSTHOG_DEBUG === "1", // Debug mode based on the environment variable autocapture: false, capture_pageview: false, // Disable automatic pageview capture, as we capture manually }); From 7b76df68680f08eb416c93f24ba9ac2d7b7a1eee Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:26:34 +0530 Subject: [PATCH 037/308] [WEB-581] chore: project states setting page improvement (#3864) * chore: project states setting page improvement * chore: code refactor * chore: observer added in project state setting page --- .../projects/[projectId]/settings/states.tsx | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx index 3fa9561a8..57451e699 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx @@ -1,21 +1,34 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; // layout import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; // components import { ProjectSettingStateList } from "components/states"; import { ProjectSettingHeader } from "components/headers"; +import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; +// hook +import { useProject } from "hooks/store"; -const StatesSettingsPage: NextPageWithLayout = () => ( -
    -
    -

    States

    -
    - -
    -); +const StatesSettingsPage: NextPageWithLayout = observer(() => { + // store + const { currentProjectDetails } = useProject(); + // derived values + const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - States` : undefined; + return ( + <> + +
    +
    +

    States

    +
    + +
    + + ); +}); StatesSettingsPage.getLayout = function getLayout(page: ReactElement) { return ( From 666b7ea5775a0e24b72584725bbb78aac09eeb5b Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:27:41 +0530 Subject: [PATCH 038/308] fix: remove member button alignment (#3862) --- .../send-workspace-invitation-modal.tsx | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/web/components/workspace/send-workspace-invitation-modal.tsx b/web/components/workspace/send-workspace-invitation-modal.tsx index 35b5963d0..25f4c3c72 100644 --- a/web/components/workspace/send-workspace-invitation-modal.tsx +++ b/web/components/workspace/send-workspace-invitation-modal.tsx @@ -121,7 +121,7 @@ export const SendWorkspaceInvitationModal: React.FC = observer((props) =>
    {fields.map((field, index) => ( -
    +
    = observer((props) => )} />
    -
    +
    = observer((props) => label={{ROLE[value]}} onChange={onChange} optionsClassName="w-full" + className="flex-grow" input > {Object.entries(ROLE).map(([key, value]) => { @@ -179,16 +180,16 @@ export const SendWorkspaceInvitationModal: React.FC = observer((props) => )} /> + {fields.length > 1 && ( + + )}
    - {fields.length > 1 && ( - - )}
    ))}
    From 69fa1708cc79363de38231c4d9f2de544a0aa8b8 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:27:59 +0530 Subject: [PATCH 039/308] fix: module quick action dropdown overflow fix (#3865) --- web/components/modules/module-card-item.tsx | 2 +- web/components/modules/module-list-item.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/modules/module-card-item.tsx b/web/components/modules/module-card-item.tsx index 52cc6097b..dbbde56d7 100644 --- a/web/components/modules/module-card-item.tsx +++ b/web/components/modules/module-card-item.tsx @@ -265,7 +265,7 @@ export const ModuleCardItem: React.FC = observer((props) => { ))} - + {isEditingAllowed && ( <> diff --git a/web/components/modules/module-list-item.tsx b/web/components/modules/module-list-item.tsx index 72ed16adf..63e780cb2 100644 --- a/web/components/modules/module-list-item.tsx +++ b/web/components/modules/module-list-item.tsx @@ -255,7 +255,7 @@ export const ModuleListItem: React.FC = observer((props) => { ))} - + {isEditingAllowed && ( <> From 2b05d23470770de4e15463cd1961c6491550015b Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:28:24 +0530 Subject: [PATCH 040/308] fix: kanban card state overflow fix (#3866) --- .../issues/issue-layouts/properties/all-properties.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/issues/issue-layouts/properties/all-properties.tsx b/web/components/issues/issue-layouts/properties/all-properties.tsx index 776a1cd46..238d2e744 100644 --- a/web/components/issues/issue-layouts/properties/all-properties.tsx +++ b/web/components/issues/issue-layouts/properties/all-properties.tsx @@ -240,7 +240,7 @@ export const IssueProperties: React.FC = observer((props) => { {/* basic properties */} {/* state */} -
    +
    Date: Wed, 6 Mar 2024 14:29:52 +0530 Subject: [PATCH 041/308] [WEB-566] feat: Added text to empty color picker boxes and other fixes (#3867) * fix: z index issues with modals and on hover color in table item picker menu * feat: added text indicators inside the table colors to give a gist of how text would look --- packages/editor/core/src/styles/table.css | 6 +++--- .../core/src/ui/extensions/table/table/table-view.tsx | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/editor/core/src/styles/table.css b/packages/editor/core/src/styles/table.css index ca384d34f..3ba17ee1b 100644 --- a/packages/editor/core/src/styles/table.css +++ b/packages/editor/core/src/styles/table.css @@ -98,7 +98,7 @@ top: 0; bottom: -2px; width: 4px; - z-index: 99; + z-index: 5; background-color: #d9e4ff; pointer-events: none; } @@ -111,7 +111,7 @@ .tableWrapper .tableControls .rowsControl { transition: opacity ease-in 100ms; position: absolute; - z-index: 99; + z-index: 5; display: flex; justify-content: center; align-items: center; @@ -198,7 +198,7 @@ .tableWrapper .tableControls .tableToolbox .toolboxItem:hover, .tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem:hover { - background-color: rgba(var(--color-background-100), 0.5); + background-color: rgba(var(--color-background-80), 0.6); } .tableWrapper .tableControls .tableToolbox .toolboxItem .iconContainer, diff --git a/packages/editor/core/src/ui/extensions/table/table/table-view.tsx b/packages/editor/core/src/ui/extensions/table/table/table-view.tsx index 674a8e115..2941179c7 100644 --- a/packages/editor/core/src/ui/extensions/table/table/table-view.tsx +++ b/packages/editor/core/src/ui/extensions/table/table/table-view.tsx @@ -213,10 +213,11 @@ function createToolbox({ { className: "colorPicker grid" }, Object.entries(colors).map(([colorName, colorValue]) => h("div", { - className: "colorPickerItem", + className: "colorPickerItem flex items-center justify-center", style: `background-color: ${colorValue.backgroundColor}; - color: ${colorValue.textColor || "inherit"};`, - innerHTML: colorValue?.icon || "", + color: ${colorValue.textColor || "inherit"};`, + innerHTML: + colorValue.icon ?? `A`, onClick: () => onSelectColor(colorValue), }) ) From 3d09a69d5803369fe2945de51dacf0e52d46bce6 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Wed, 6 Mar 2024 18:39:14 +0530 Subject: [PATCH 042/308] fix: eslint issues and reconfiguring (#3891) * fix: eslint fixes --------- Co-authored-by: gurusainath --- CONTRIBUTING.md | 1 - ENV_SETUP.md | 1 - README.md | 36 +- deploy/1-click/README.md | 30 +- packages/editor/core/package.json | 3 +- packages/editor/document-editor/package.json | 3 +- .../src/ui/components/content-browser.tsx | 2 +- .../src/ui/components/summary-popover.tsx | 5 +- .../src/ui/extensions/index.tsx | 1 - packages/editor/extensions/package.json | 3 +- packages/editor/lite-text-editor/package.json | 3 +- packages/editor/rich-text-editor/package.json | 2 +- packages/eslint-config-custom/index.js | 33 +- packages/eslint-config-custom/package.json | 22 +- packages/types/src/pages.d.ts | 7 +- packages/types/src/state.d.ts | 7 +- packages/types/src/views.d.ts | 6 +- packages/ui/src/badge/helper.tsx | 10 +- packages/ui/src/breadcrumbs/breadcrumbs.tsx | 15 +- packages/ui/src/button/helper.tsx | 10 +- packages/ui/src/control-link/control-link.tsx | 4 +- space/components/ui/dropdown.tsx | 6 +- space/lib/mobx/store-provider.tsx | 8 +- space/package.json | 2 - web/.eslintrc.js | 99 +++ .../account/deactivate-account-modal.tsx | 8 +- .../account/o-auth/o-auth-options.tsx | 6 +- .../sign-in-forms/optional-set-password.tsx | 2 +- .../account/sign-in-forms/password.tsx | 12 +- web/components/account/sign-in-forms/root.tsx | 12 +- .../account/sign-in-forms/unique-code.tsx | 12 +- .../account/sign-up-forms/email.tsx | 6 +- .../sign-up-forms/optional-set-password.tsx | 14 +- .../account/sign-up-forms/password.tsx | 6 +- web/components/account/sign-up-forms/root.tsx | 10 +- .../account/sign-up-forms/unique-code.tsx | 11 +- .../custom-analytics/custom-analytics.tsx | 10 +- .../custom-analytics/graph/custom-tooltip.tsx | 4 +- .../custom-analytics/graph/index.tsx | 8 +- .../custom-analytics/main-content.tsx | 4 +- .../analytics/custom-analytics/select-bar.tsx | 7 +- .../custom-analytics/select/project.tsx | 2 +- .../custom-analytics/select/segment.tsx | 2 +- .../custom-analytics/select/x-axis.tsx | 2 +- .../custom-analytics/select/y-axis.tsx | 2 +- .../sidebar/projects-list.tsx | 2 +- .../sidebar/sidebar-header.tsx | 8 +- .../custom-analytics/sidebar/sidebar.tsx | 20 +- .../analytics/custom-analytics/table.tsx | 2 +- .../analytics/project-modal/main-content.tsx | 9 +- .../analytics/project-modal/modal.tsx | 10 +- .../analytics/scope-and-demand/demand.tsx | 2 +- .../scope-and-demand/scope-and-demand.tsx | 4 +- .../scope-and-demand/year-wise-issues.tsx | 2 +- .../api-token/delete-token-modal.tsx | 4 +- .../api-token/modal/create-token-modal.tsx | 11 +- web/components/api-token/token-list-item.tsx | 2 +- .../auth-screens/not-authorized-view.tsx | 6 +- .../auth-screens/project/join-project.tsx | 4 +- .../auth-screens/workspace/not-a-member.tsx | 2 +- .../automation/auto-archive-automation.tsx | 4 +- .../automation/auto-close-automation.tsx | 8 +- web/components/breadcrumbs/index.tsx | 2 +- .../command-palette/actions/help-actions.tsx | 2 +- .../actions/issue-actions/actions-list.tsx | 10 +- .../actions/issue-actions/change-assignee.tsx | 8 +- .../actions/issue-actions/change-priority.tsx | 8 +- .../actions/issue-actions/change-state.tsx | 10 +- .../actions/project-actions.tsx | 2 +- .../actions/search-results.tsx | 2 +- .../command-palette/actions/theme-actions.tsx | 6 +- .../actions/workspace-settings-actions.tsx | 4 +- .../command-palette/command-modal.tsx | 22 +- .../command-palette/command-palette.tsx | 15 +- web/components/command-palette/helpers.tsx | 2 +- .../command-palette/shortcuts-modal/modal.tsx | 2 +- web/components/common/breadcrumb-link.tsx | 2 +- .../common/product-updates-modal.tsx | 8 +- web/components/core/activity.tsx | 6 +- .../core/filters/date-filter-modal.tsx | 15 +- web/components/core/image-picker-popover.tsx | 12 +- .../core/modals/bulk-delete-issues-modal.tsx | 15 +- .../modals/existing-issues-list-modal.tsx | 13 +- .../core/modals/gpt-assistant-popover.tsx | 14 +- web/components/core/modals/link-modal.tsx | 4 +- .../core/modals/user-image-upload-modal.tsx | 7 +- .../modals/workspace-image-upload-modal.tsx | 8 +- web/components/core/render-if-visible-HOC.tsx | 11 +- web/components/core/sidebar/links-list.tsx | 11 +- .../sidebar/sidebar-menu-hamburger-toggle.tsx | 2 +- .../core/sidebar/sidebar-progress-stats.tsx | 4 +- .../core/theme/color-picker-input.tsx | 4 +- .../core/theme/custom-theme-selector.tsx | 4 +- web/components/core/theme/theme-switch.tsx | 2 +- .../cycles/active-cycle-details.tsx | 20 +- web/components/cycles/active-cycle-stats.tsx | 4 +- web/components/cycles/cycle-mobile-header.tsx | 9 +- web/components/cycles/cycle-peek-overview.tsx | 2 +- web/components/cycles/cycles-board-card.tsx | 18 +- web/components/cycles/cycles-board.tsx | 2 +- web/components/cycles/cycles-list-item.tsx | 24 +- web/components/cycles/cycles-list.tsx | 4 +- web/components/cycles/cycles-view.tsx | 10 +- web/components/cycles/delete-modal.tsx | 6 +- web/components/cycles/form.tsx | 2 +- web/components/cycles/gantt-chart/blocks.tsx | 28 +- .../cycles/gantt-chart/cycles-list-layout.tsx | 8 +- web/components/cycles/modal.tsx | 10 +- web/components/cycles/sidebar.tsx | 35 +- .../cycles/transfer-issues-modal.tsx | 11 +- web/components/cycles/transfer-issues.tsx | 6 +- .../dashboard/home-dashboard-widgets.tsx | 4 +- .../dashboard/project-empty-state.tsx | 6 +- .../dashboard/widgets/assigned-issues.tsx | 10 +- .../dashboard/widgets/created-issues.tsx | 10 +- .../widgets/dropdowns/duration-filter.tsx | 3 +- .../widgets/empty-states/assigned-issues.tsx | 2 +- .../widgets/empty-states/created-issues.tsx | 2 +- .../widgets/issue-panels/issue-list-item.tsx | 4 +- .../widgets/issue-panels/issues-list.tsx | 4 +- .../widgets/issue-panels/tabs-list.tsx | 2 +- .../dashboard/widgets/issues-by-priority.tsx | 16 +- .../widgets/issues-by-state-group.tsx | 30 +- .../dashboard/widgets/loaders/loader.tsx | 4 +- .../dashboard/widgets/overview-stats.tsx | 7 +- .../dashboard/widgets/recent-activity.tsx | 20 +- .../widgets/recent-collaborators.tsx | 94 +++ .../collaborators-list.tsx | 6 +- .../recent-collaborators/default-list.tsx | 2 +- .../widgets/recent-collaborators/root.tsx | 11 +- .../recent-collaborators/search-list.tsx | 2 +- .../dashboard/widgets/recent-projects.tsx | 12 +- web/components/dropdowns/buttons.tsx | 8 +- .../dropdowns/cycle/cycle-options.tsx | 30 +- web/components/dropdowns/cycle/index.tsx | 6 +- web/components/dropdowns/date-range.tsx | 10 +- web/components/dropdowns/date.tsx | 8 +- web/components/dropdowns/estimate.tsx | 12 +- web/components/dropdowns/member/avatar.tsx | 2 +- web/components/dropdowns/member/index.tsx | 8 +- .../dropdowns/member/member-options.tsx | 8 +- web/components/dropdowns/module/index.tsx | 17 +- .../dropdowns/module/module-options.tsx | 10 +- web/components/dropdowns/priority.tsx | 18 +- web/components/dropdowns/project.tsx | 16 +- web/components/dropdowns/state.tsx | 8 +- web/components/emoji-icon-picker/index.tsx | 8 +- .../empty-state/comic-box-button.tsx | 2 +- web/components/empty-state/empty-state.tsx | 2 +- .../create-update-estimate-modal.tsx | 8 +- .../estimates/delete-estimate-modal.tsx | 5 +- .../estimates/estimate-list-item.tsx | 8 +- web/components/estimates/estimates-list.tsx | 12 +- web/components/exporter/export-modal.tsx | 5 +- web/components/exporter/guide.tsx | 21 +- web/components/exporter/single-export.tsx | 12 +- web/components/gantt-chart/blocks/block.tsx | 8 +- .../gantt-chart/blocks/blocks-list.tsx | 5 +- web/components/gantt-chart/chart/header.tsx | 6 +- .../gantt-chart/chart/main-content.tsx | 2 +- web/components/gantt-chart/chart/root.tsx | 18 +- .../gantt-chart/chart/views/month.tsx | 2 +- web/components/gantt-chart/contexts/index.tsx | 16 +- .../gantt-chart/helpers/add-block.tsx | 4 +- .../gantt-chart/helpers/draggable.tsx | 2 +- .../gantt-chart/sidebar/cycles/block.tsx | 6 +- .../gantt-chart/sidebar/cycles/sidebar.tsx | 2 +- .../gantt-chart/sidebar/issues/block.tsx | 4 +- .../gantt-chart/sidebar/issues/sidebar.tsx | 2 +- .../gantt-chart/sidebar/modules/block.tsx | 4 +- .../gantt-chart/sidebar/modules/sidebar.tsx | 2 +- .../gantt-chart/sidebar/project-views.tsx | 2 +- .../gantt-chart/views/bi-week-view.ts | 2 +- web/components/gantt-chart/views/day-view.ts | 2 +- web/components/gantt-chart/views/helpers.ts | 4 +- .../gantt-chart/views/hours-view.ts | 2 +- .../gantt-chart/views/month-view.ts | 4 +- .../gantt-chart/views/quater-view.ts | 2 +- web/components/gantt-chart/views/week-view.ts | 2 +- web/components/gantt-chart/views/year-view.ts | 2 +- web/components/graphs/issues-by-priority.tsx | 6 +- web/components/headers/cycle-issues.tsx | 30 +- web/components/headers/cycles.tsx | 17 +- web/components/headers/global-issues.tsx | 22 +- web/components/headers/module-issues.tsx | 30 +- web/components/headers/modules-list.tsx | 40 +- web/components/headers/page-details.tsx | 8 +- web/components/headers/pages.tsx | 10 +- web/components/headers/profile-settings.tsx | 2 +- .../project-archived-issue-details.tsx | 16 +- .../headers/project-archived-issues.tsx | 10 +- .../headers/project-draft-issues.tsx | 14 +- web/components/headers/project-inbox.tsx | 8 +- .../headers/project-issue-details.tsx | 22 +- web/components/headers/project-issues.tsx | 22 +- web/components/headers/project-settings.tsx | 8 +- .../headers/project-view-issues.tsx | 30 +- web/components/headers/project-views.tsx | 8 +- web/components/headers/projects.tsx | 6 +- web/components/headers/user-profile.tsx | 111 +-- .../headers/workspace-active-cycles.tsx | 2 +- .../headers/workspace-analytics.tsx | 26 +- .../headers/workspace-dashboard.tsx | 6 +- web/components/headers/workspace-settings.tsx | 6 +- web/components/icons/priority-icon.tsx | 12 +- .../icons/state/state-group-icon.tsx | 2 +- web/components/inbox/content/root.tsx | 6 +- web/components/inbox/inbox-issue-actions.tsx | 22 +- web/components/inbox/inbox-issue-status.tsx | 2 +- .../inbox/modals/accept-issue-modal.tsx | 2 +- .../inbox/modals/create-issue-modal.tsx | 18 +- .../inbox/modals/decline-issue-modal.tsx | 2 +- .../inbox/modals/delete-issue-modal.tsx | 2 +- .../inbox/modals/select-duplicate.tsx | 8 +- .../inbox/sidebar/filter/applied-filters.tsx | 16 +- .../inbox/sidebar/filter/filter-selection.tsx | 6 +- .../inbox/sidebar/inbox-list-item.tsx | 4 +- web/components/inbox/sidebar/inbox-list.tsx | 8 +- web/components/inbox/sidebar/root.tsx | 4 +- web/components/instance/ai-form.tsx | 1 - web/components/instance/email-form.tsx | 3 +- web/components/instance/general-form.tsx | 1 - .../instance/github-config-form.tsx | 1 - .../instance/google-config-form.tsx | 1 - web/components/instance/help-section.tsx | 4 +- web/components/instance/image-config-form.tsx | 3 +- .../instance/instance-admin-restriction.tsx | 6 +- web/components/instance/not-ready-view.tsx | 2 +- web/components/instance/setup-done-view.tsx | 4 +- .../instance/setup-form/sign-in-form.tsx | 6 +- web/components/instance/sidebar-dropdown.tsx | 6 +- web/components/instance/sidebar-menu.tsx | 2 +- .../integration/delete-import-modal.tsx | 6 +- web/components/integration/github/auth.tsx | 2 +- .../integration/github/import-data.tsx | 4 +- .../integration/github/repo-details.tsx | 5 +- web/components/integration/github/root.tsx | 15 +- .../integration/github/select-repository.tsx | 2 +- .../integration/github/single-user-select.tsx | 4 +- web/components/integration/guide.tsx | 24 +- .../integration/jira/give-details.tsx | 4 +- .../integration/jira/import-users.tsx | 6 +- .../integration/jira/jira-project-detail.tsx | 6 +- web/components/integration/jira/root.tsx | 16 +- web/components/integration/single-import.tsx | 12 +- .../integration/single-integration-card.tsx | 16 +- .../integration/slack/select-channel.tsx | 6 +- web/components/issues/archive-issue-modal.tsx | 6 +- .../issues/attachment/attachment-detail.tsx | 8 +- .../issues/attachment/attachment-upload.tsx | 2 +- .../issues/attachment/attachments-list.tsx | 1 + .../delete-attachment-confirmation-modal.tsx | 2 +- web/components/issues/attachment/root.tsx | 2 +- web/components/issues/delete-issue-modal.tsx | 2 - web/components/issues/description-form.tsx | 18 +- web/components/issues/description-input.tsx | 9 +- .../issues/issue-detail/cycle-select.tsx | 7 +- .../issues/issue-detail/inbox/index.ts | 6 +- .../issue-detail/inbox/main-content.tsx | 14 +- .../issues/issue-detail/inbox/root.tsx | 16 +- .../issues/issue-detail/inbox/sidebar.tsx | 8 +- .../issue-activity/activity-comment-root.tsx | 1 + .../activity/actions/archived-at.tsx | 2 +- .../activity/actions/assignee.tsx | 4 +- .../issue-activity/activity/actions/cycle.tsx | 2 +- .../activity/actions/default.tsx | 2 +- .../activity/actions/estimate.tsx | 2 - .../actions/helpers/activity-block.tsx | 6 +- .../activity/actions/helpers/issue-link.tsx | 2 +- .../activity/actions/module.tsx | 2 +- .../activity/actions/relation.tsx | 4 +- .../activity/actions/start_date.tsx | 2 +- .../issue-activity/activity/actions/state.tsx | 2 +- .../activity/actions/target_date.tsx | 2 +- .../issue-activity/activity/root.tsx | 1 + .../issue-activity/comments/comment-block.tsx | 4 +- .../issue-activity/comments/comment-card.tsx | 14 +- .../comments/comment-create.tsx | 12 +- .../issue-activity/comments/root.tsx | 3 +- .../issue-detail/issue-activity/root.tsx | 4 +- .../issue-detail/label/create-label.tsx | 12 +- .../issue-detail/label/label-list-item.tsx | 2 +- .../issues/issue-detail/label/label-list.tsx | 3 +- .../issues/issue-detail/label/root.tsx | 4 +- .../label/select/label-select.tsx | 10 +- .../issues/issue-detail/label/select/root.tsx | 2 +- .../links/create-update-link-modal.tsx | 4 +- .../issues/issue-detail/links/link-detail.tsx | 8 +- .../issues/issue-detail/links/links.tsx | 3 +- .../issues/issue-detail/links/root.tsx | 4 +- .../issues/issue-detail/main-content.tsx | 10 +- .../issues/issue-detail/module-select.tsx | 9 +- .../issues/issue-detail/parent-select.tsx | 6 +- .../issues/issue-detail/parent/root.tsx | 4 +- .../issues/issue-detail/parent/siblings.tsx | 6 +- .../issue-detail/reactions/issue-comment.tsx | 17 +- .../issues/issue-detail/reactions/issue.tsx | 7 +- .../reactions/reaction-selector.tsx | 2 +- .../issues/issue-detail/relation-select.tsx | 18 +- web/components/issues/issue-detail/root.tsx | 29 +- .../issues/issue-detail/sidebar.tsx | 136 ++-- .../issues/issue-detail/subscription.tsx | 6 +- .../calendar/base-calendar-root.tsx | 16 +- .../issue-layouts/calendar/calendar.tsx | 12 +- .../issue-layouts/calendar/day-tile.tsx | 6 +- .../calendar/dropdowns/months-dropdown.tsx | 4 +- .../calendar/dropdowns/options-dropdown.tsx | 10 +- .../issues/issue-layouts/calendar/header.tsx | 2 +- .../issue-layouts/calendar/issue-blocks.tsx | 6 +- .../calendar/quick-add-issue-form.tsx | 10 +- .../calendar/roots/cycle-root.tsx | 8 +- .../calendar/roots/module-root.tsx | 8 +- .../calendar/roots/project-root.tsx | 10 +- .../calendar/roots/project-view-root.tsx | 8 +- .../issue-layouts/calendar/week-days.tsx | 4 +- .../empty-states/archived-issues.tsx | 10 +- .../issue-layouts/empty-states/cycle.tsx | 16 +- .../empty-states/draft-issues.tsx | 10 +- .../empty-states/global-view.tsx | 2 +- .../issue-layouts/empty-states/module.tsx | 14 +- .../empty-states/project-issues.tsx | 10 +- .../empty-states/project-view.tsx | 4 +- .../filters/applied-filters/cycle.tsx | 2 +- .../filters/applied-filters/date.tsx | 2 +- .../filters/applied-filters/filters-list.tsx | 9 +- .../filters/applied-filters/module.tsx | 2 +- .../filters/applied-filters/priority.tsx | 2 +- .../filters/applied-filters/project.tsx | 2 +- .../applied-filters/roots/archived-issue.tsx | 6 +- .../applied-filters/roots/cycle-root.tsx | 6 +- .../applied-filters/roots/draft-issue.tsx | 6 +- .../roots/global-view-root.tsx | 10 +- .../applied-filters/roots/module-root.tsx | 6 +- .../roots/profile-issues-root.tsx | 6 +- .../applied-filters/roots/project-root.tsx | 6 +- .../roots/project-view-root.tsx | 10 +- .../filters/applied-filters/state-group.tsx | 2 +- .../filters/applied-filters/state.tsx | 2 +- .../display-filters-selection.tsx | 4 +- .../display-filters/display-properties.tsx | 4 +- .../header/display-filters/extra-options.tsx | 2 +- .../header/display-filters/group-by.tsx | 2 +- .../header/display-filters/issue-type.tsx | 2 +- .../header/display-filters/order-by.tsx | 2 +- .../header/display-filters/sub-group-by.tsx | 2 +- .../filters/header/filters/assignee.tsx | 8 +- .../filters/header/filters/created-by.tsx | 8 +- .../filters/header/filters/cycle.tsx | 4 +- .../header/filters/filters-selection.tsx | 6 +- .../filters/header/filters/labels.tsx | 2 +- .../filters/header/filters/mentions.tsx | 8 +- .../filters/header/filters/module.tsx | 4 +- .../filters/header/filters/project.tsx | 4 +- .../filters/header/filters/start-date.tsx | 2 +- .../filters/header/filters/state-group.tsx | 2 +- .../filters/header/filters/state.tsx | 2 +- .../filters/header/filters/target-date.tsx | 2 +- .../filters/header/helpers/dropdown.tsx | 40 +- .../filters/header/layout-selection.tsx | 2 +- .../issue-layouts/gantt/base-gantt-root.tsx | 12 +- .../issues/issue-layouts/gantt/blocks.tsx | 2 +- .../issues/issue-layouts/gantt/cycle-root.tsx | 6 +- .../issue-layouts/gantt/module-root.tsx | 6 +- .../issue-layouts/gantt/project-root.tsx | 8 +- .../issue-layouts/gantt/project-view-root.tsx | 8 +- .../gantt/quick-add-issue-form.tsx | 11 +- .../issue-layouts/kanban/base-kanban-root.tsx | 41 +- .../issues/issue-layouts/kanban/block.tsx | 16 +- .../issue-layouts/kanban/blocks-list.tsx | 2 +- .../issues/issue-layouts/kanban/default.tsx | 34 +- .../kanban/headers/group-by-card.tsx | 14 +- .../kanban/headers/sub-group-by-card.tsx | 2 +- .../issue-layouts/kanban/kanban-group.tsx | 2 +- .../kanban/quick-add-issue-form.tsx | 10 +- .../issue-layouts/kanban/roots/cycle-root.tsx | 8 +- .../kanban/roots/draft-issue-root.tsx | 10 +- .../kanban/roots/module-root.tsx | 8 +- .../kanban/roots/profile-issues-root.tsx | 12 +- .../kanban/roots/project-root.tsx | 12 +- .../kanban/roots/project-view-root.tsx | 6 +- .../issues/issue-layouts/kanban/swimlanes.tsx | 24 +- .../issue-layouts/list/base-list-root.tsx | 16 +- .../issues/issue-layouts/list/block.tsx | 4 +- .../issues/issue-layouts/list/blocks-list.tsx | 2 +- .../issues/issue-layouts/list/default.tsx | 12 +- .../list/headers/group-by-card.tsx | 12 +- .../list/quick-add-issue-form.tsx | 10 +- .../list/roots/archived-issue-root.tsx | 8 +- .../issue-layouts/list/roots/cycle-root.tsx | 8 +- .../list/roots/draft-issue-root.tsx | 6 +- .../issue-layouts/list/roots/module-root.tsx | 6 +- .../list/roots/profile-issues-root.tsx | 8 +- .../issue-layouts/list/roots/project-root.tsx | 6 +- .../list/roots/project-view-root.tsx | 6 +- .../properties/all-properties.tsx | 26 +- .../issue-layouts/properties/labels.tsx | 10 +- .../with-display-properties-HOC.tsx | 2 +- .../quick-action-dropdowns/all-issue.tsx | 17 +- .../quick-action-dropdowns/archived-issue.tsx | 14 +- .../quick-action-dropdowns/cycle-issue.tsx | 15 +- .../quick-action-dropdowns/module-issue.tsx | 15 +- .../quick-action-dropdowns/project-issue.tsx | 16 +- .../roots/all-issue-layout-root.tsx | 34 +- .../roots/archived-issue-layout-root.tsx | 6 +- .../issue-layouts/roots/cycle-layout-root.tsx | 12 +- .../roots/draft-issue-layout-root.tsx | 18 +- .../roots/module-layout-root.tsx | 8 +- .../roots/project-layout-root.tsx | 8 +- .../roots/project-view-layout-root.tsx | 4 +- .../spreadsheet/base-spreadsheet-root.tsx | 16 +- .../spreadsheet/columns/cycle-column.tsx | 6 +- .../spreadsheet/columns/due-date-column.tsx | 4 +- .../spreadsheet/columns/estimate-column.tsx | 2 +- .../spreadsheet/columns/header-column.tsx | 4 +- .../spreadsheet/columns/label-column.tsx | 2 +- .../spreadsheet/columns/module-column.tsx | 10 +- .../spreadsheet/columns/priority-column.tsx | 2 +- .../spreadsheet/columns/sub-issue-column.tsx | 2 +- .../spreadsheet/issue-column.tsx | 8 +- .../issue-layouts/spreadsheet/issue-row.tsx | 22 +- .../spreadsheet/quick-add-issue-form.tsx | 8 +- .../spreadsheet/roots/cycle-root.tsx | 6 +- .../spreadsheet/roots/module-root.tsx | 6 +- .../spreadsheet/roots/project-root.tsx | 6 +- .../spreadsheet/roots/project-view-root.tsx | 10 +- .../spreadsheet/spreadsheet-header-column.tsx | 2 +- .../spreadsheet/spreadsheet-header.tsx | 3 +- .../spreadsheet/spreadsheet-table.tsx | 18 +- .../spreadsheet/spreadsheet-view.tsx | 6 +- web/components/issues/issue-layouts/utils.tsx | 36 +- .../issues/issue-modal/draft-issue-layout.tsx | 8 +- web/components/issues/issue-modal/form.tsx | 40 +- web/components/issues/issue-modal/modal.tsx | 14 +- web/components/issues/issue-update-status.tsx | 2 +- .../issues/issues-mobile-header.tsx | 29 +- .../issues/parent-issues-list-modal.tsx | 8 +- .../issues/peek-overview/header.tsx | 10 +- .../issues/peek-overview/issue-detail.tsx | 4 +- .../issues/peek-overview/properties.tsx | 6 +- web/components/issues/peek-overview/root.tsx | 15 +- web/components/issues/peek-overview/view.tsx | 20 +- web/components/issues/select/label.tsx | 10 +- .../issues/sub-issues/issue-list-item.tsx | 8 +- .../issues/sub-issues/issues-list.tsx | 2 +- .../issues/sub-issues/properties.tsx | 2 +- web/components/issues/sub-issues/root.tsx | 12 +- web/components/issues/title-input.tsx | 2 +- web/components/labels/create-label-modal.tsx | 8 +- .../labels/create-update-label-inline.tsx | 11 +- web/components/labels/delete-label-modal.tsx | 6 +- .../labels/label-block/label-item-block.tsx | 4 +- .../labels/project-setting-label-group.tsx | 24 +- .../labels/project-setting-label-item.tsx | 4 +- .../labels/project-setting-label-list.tsx | 42 +- .../modules/delete-module-modal.tsx | 8 +- web/components/modules/form.tsx | 6 +- web/components/modules/gantt-chart/blocks.tsx | 6 +- .../gantt-chart/modules-list-layout.tsx | 4 +- web/components/modules/modal.tsx | 14 +- web/components/modules/module-card-item.tsx | 20 +- web/components/modules/module-list-item.tsx | 34 +- .../modules/module-mobile-header.tsx | 31 +- .../modules/module-peek-overview.tsx | 2 +- web/components/modules/modules-list-view.tsx | 10 +- web/components/modules/select/status.tsx | 2 +- .../modules/sidebar-select/select-status.tsx | 2 +- web/components/modules/sidebar.tsx | 32 +- .../notifications/notification-card.tsx | 33 +- .../notifications/notification-header.tsx | 18 +- .../notifications/notification-popover.tsx | 14 +- .../select-snooze-till-modal.tsx | 18 +- web/components/onboarding/invitations.tsx | 23 +- web/components/onboarding/invite-members.tsx | 28 +- web/components/onboarding/join-workspaces.tsx | 4 +- .../onboarding/onboarding-sidebar.tsx | 9 +- .../switch-delete-account-modal.tsx | 4 +- web/components/onboarding/tour/root.tsx | 14 +- web/components/onboarding/tour/sidebar.tsx | 2 +- web/components/onboarding/user-details.tsx | 24 +- web/components/onboarding/workspace.tsx | 7 +- web/components/page-views/signin.tsx | 6 +- .../page-views/workspace-dashboard.tsx | 16 +- .../pages/create-update-page-modal.tsx | 8 +- web/components/pages/delete-page-modal.tsx | 10 +- web/components/pages/page-form.tsx | 2 +- .../pages/pages-list/all-pages-list.tsx | 2 +- .../pages/pages-list/archived-pages-list.tsx | 2 +- .../pages/pages-list/favorite-pages-list.tsx | 2 +- web/components/pages/pages-list/list-item.tsx | 10 +- web/components/pages/pages-list/list-view.tsx | 8 +- .../pages/pages-list/private-page-list.tsx | 2 +- .../pages/pages-list/recent-pages-list.tsx | 12 +- .../pages/pages-list/shared-pages-list.tsx | 2 +- .../profile/activity/activity-list.tsx | 6 +- .../profile/activity/download-button.tsx | 2 +- .../activity/profile-activity-list.tsx | 12 +- .../activity/workspace-activity-list.tsx | 2 +- web/components/profile/navbar.tsx | 2 +- web/components/profile/overview/activity.tsx | 14 +- .../overview/priority-distribution.tsx | 88 ++ .../priority-distribution.tsx | 2 +- .../profile/overview/state-distribution.tsx | 2 +- web/components/profile/overview/stats.tsx | 4 +- web/components/profile/overview/workload.tsx | 2 +- web/components/profile/preferences/index.ts | 2 +- .../profile/profile-issues-filter.tsx | 2 +- web/components/profile/profile-issues.tsx | 22 +- web/components/profile/sidebar.tsx | 33 +- web/components/project/card-list.tsx | 6 +- web/components/project/card.tsx | 15 +- .../project/confirm-project-member-remove.tsx | 8 +- .../project/create-project-modal.tsx | 19 +- .../project/delete-project-modal.tsx | 6 +- web/components/project/form.tsx | 25 +- web/components/project/integration-card.tsx | 15 +- web/components/project/join-project-modal.tsx | 2 +- .../project/leave-project-modal.tsx | 12 +- web/components/project/member-list-item.tsx | 20 +- web/components/project/member-list.tsx | 4 +- web/components/project/member-select.tsx | 2 +- .../project-settings-member-defaults.tsx | 19 +- .../project/publish-project/modal.tsx | 20 +- .../project/send-project-invitation-modal.tsx | 8 +- .../settings/delete-project-section.tsx | 2 +- .../project/settings/features-list.tsx | 16 +- web/components/project/sidebar-list-item.tsx | 21 +- web/components/project/sidebar-list.tsx | 22 +- web/components/states/create-state-modal.tsx | 20 +- .../states/create-update-state-inline.tsx | 18 +- web/components/states/delete-state-modal.tsx | 10 +- .../project-setting-state-list-item.tsx | 6 +- .../states/project-setting-state-list.tsx | 14 +- web/components/toast-alert/index.tsx | 61 ++ web/components/ui/empty-space.tsx | 2 +- web/components/ui/graphs/bar-graph.tsx | 2 +- web/components/ui/graphs/calendar-graph.tsx | 2 +- web/components/ui/graphs/line-graph.tsx | 2 +- web/components/ui/graphs/marimekko-graph.tsx | 48 ++ web/components/ui/graphs/pie-graph.tsx | 2 +- .../ui/graphs/scatter-plot-graph.tsx | 2 +- .../ui/loader/cycle-module-board-loader.tsx | 7 +- .../ui/loader/cycle-module-list-loader.tsx | 7 +- .../project-inbox/inbox-layout-loader.tsx | 2 +- .../project-inbox/inbox-sidebar-loader.tsx | 4 +- .../ui/loader/notification-loader.tsx | 4 +- web/components/ui/loader/pages-loader.tsx | 8 +- web/components/ui/loader/projects-loader.tsx | 7 +- .../ui/loader/settings/activity.tsx | 4 +- .../ui/loader/settings/api-token.tsx | 4 +- web/components/ui/loader/settings/email.tsx | 4 +- .../ui/loader/settings/import-and-export.tsx | 4 +- .../ui/loader/settings/integration.tsx | 7 +- web/components/ui/loader/settings/members.tsx | 4 +- web/components/ui/loader/view-list-loader.tsx | 4 +- web/components/ui/multi-level-dropdown.tsx | 8 +- web/components/views/delete-view-modal.tsx | 6 +- web/components/views/form.tsx | 10 +- web/components/views/modal.tsx | 4 +- web/components/views/view-list-item.tsx | 14 +- web/components/views/views-list.tsx | 10 +- .../web-hooks/create-webhook-modal.tsx | 12 +- .../web-hooks/delete-webhook-modal.tsx | 4 +- web/components/web-hooks/form/form.tsx | 10 +- web/components/web-hooks/form/secret-key.tsx | 19 +- .../web-hooks/generated-hook-details.tsx | 2 +- .../web-hooks/webhooks-list-item.tsx | 2 +- .../confirm-workspace-member-remove.tsx | 6 +- .../workspace/create-workspace-form.tsx | 15 +- .../workspace/delete-workspace-modal.tsx | 12 +- web/components/workspace/help-section.tsx | 74 +- .../send-workspace-invitation-modal.tsx | 8 +- .../settings/invitations-list-item.tsx | 10 +- .../workspace/settings/members-list-item.tsx | 13 +- .../workspace/settings/members-list.tsx | 9 +- .../workspace/settings/workspace-details.tsx | 20 +- web/components/workspace/sidebar-dropdown.tsx | 14 +- web/components/workspace/sidebar-menu.tsx | 14 +- .../workspace/sidebar-quick-action.tsx | 15 +- .../views/default-view-list-item.tsx | 4 +- .../workspace/views/delete-view-modal.tsx | 13 +- web/components/workspace/views/form.tsx | 16 +- web/components/workspace/views/header.tsx | 17 +- web/components/workspace/views/modal.tsx | 10 +- .../workspace/views/view-list-item.tsx | 15 +- web/components/workspace/views/views-list.tsx | 17 +- .../workspace-active-cycles-upgrade.tsx | 16 +- web/constants/cycle.ts | 2 - web/constants/dashboard.ts | 13 +- web/constants/event-tracker.ts | 20 +- web/constants/spreadsheet.ts | 6 +- web/constants/workspace.ts | 8 +- web/contexts/user-notification-context.tsx | 2 +- web/helpers/analytics.helper.ts | 26 +- web/helpers/calendar.helper.ts | 2 +- web/helpers/filter.helper.ts | 1 - web/helpers/issue.helper.ts | 12 +- web/helpers/string.helper.ts | 10 +- web/hooks/store/index.ts | 2 +- web/hooks/store/use-inbox-issues.ts | 2 +- web/hooks/store/use-issues.ts | 12 +- web/hooks/use-comment-reaction.tsx | 2 +- web/hooks/use-draggable-portal.ts | 2 +- web/hooks/use-dropdown-key-down.tsx | 8 +- web/hooks/use-user-notifications.tsx | 2 +- web/hooks/use-user.tsx | 2 +- web/layouts/admin-layout/header.tsx | 2 +- web/layouts/admin-layout/layout.tsx | 4 +- web/layouts/admin-layout/sidebar.tsx | 2 +- web/layouts/app-layout/layout.tsx | 7 +- web/layouts/app-layout/sidebar.tsx | 2 +- web/layouts/auth-layout/admin-wrapper.tsx | 2 +- web/layouts/auth-layout/project-wrapper.tsx | 10 +- web/layouts/auth-layout/user-wrapper.tsx | 4 +- web/layouts/auth-layout/workspace-wrapper.tsx | 8 +- web/layouts/instance-layout/index.tsx | 6 +- .../settings-layout/profile/layout.tsx | 2 +- .../profile/preferences/index.ts | 2 +- .../profile/preferences/layout.tsx | 10 +- .../profile/preferences/sidebar.tsx | 29 +- .../settings-layout/profile/sidebar.tsx | 6 +- .../settings-layout/project/layout.tsx | 8 +- .../settings-layout/project/sidebar.tsx | 4 +- .../settings-layout/workspace/sidebar.tsx | 4 +- web/layouts/user-profile-layout/layout.tsx | 3 +- web/lib/app-provider.tsx | 16 +- web/lib/local-storage.ts | 10 +- web/lib/posthog-provider.tsx | 10 +- web/lib/types.d.ts | 1 + web/lib/wrappers/crisp-wrapper.tsx | 8 +- web/lib/wrappers/store-wrapper.tsx | 10 +- web/package.json | 4 - web/pages/404.tsx | 6 +- web/pages/[workspaceSlug]/active-cycles.tsx | 2 +- web/pages/[workspaceSlug]/analytics.tsx | 14 +- web/pages/[workspaceSlug]/index.tsx | 6 +- .../profile/[userId]/activity.tsx | 10 +- .../profile/[userId]/assigned.tsx | 6 +- .../profile/[userId]/created.tsx | 6 +- .../profile/[userId]/index.tsx | 12 +- .../profile/[userId]/subscribed.tsx | 6 +- .../[projectId]/archived-issues/index.tsx | 10 +- .../projects/[projectId]/cycles/[cycleId].tsx | 14 +- .../projects/[projectId]/cycles/index.tsx | 24 +- .../[projectId]/draft-issues/index.tsx | 8 +- .../projects/[projectId]/inbox/[inboxId].tsx | 10 +- .../projects/[projectId]/inbox/index.tsx | 6 +- .../projects/[projectId]/issues/[issueId].tsx | 10 +- .../projects/[projectId]/issues/index.tsx | 10 +- .../[projectId]/modules/[moduleId].tsx | 14 +- .../projects/[projectId]/modules/index.tsx | 8 +- .../projects/[projectId]/pages/[pageId].tsx | 36 +- .../projects/[projectId]/pages/index.tsx | 26 +- .../[projectId]/settings/automations.tsx | 19 +- .../[projectId]/settings/estimates.tsx | 8 +- .../[projectId]/settings/features.tsx | 8 +- .../projects/[projectId]/settings/index.tsx | 12 +- .../[projectId]/settings/integrations.tsx | 22 +- .../projects/[projectId]/settings/labels.tsx | 8 +- .../projects/[projectId]/settings/members.tsx | 6 +- .../projects/[projectId]/settings/states.tsx | 8 +- .../projects/[projectId]/views/[viewId].tsx | 12 +- .../projects/[projectId]/views/index.tsx | 4 +- web/pages/[workspaceSlug]/projects/index.tsx | 4 +- .../[workspaceSlug]/settings/api-tokens.tsx | 24 +- .../[workspaceSlug]/settings/billing.tsx | 8 +- .../[workspaceSlug]/settings/exports.tsx | 8 +- .../[workspaceSlug]/settings/imports.tsx | 10 +- web/pages/[workspaceSlug]/settings/index.tsx | 8 +- .../[workspaceSlug]/settings/integrations.tsx | 16 +- .../[workspaceSlug]/settings/members.tsx | 18 +- .../settings/webhooks/[webhookId].tsx | 11 +- .../settings/webhooks/index.tsx | 18 +- .../workspace-views/[globalViewId].tsx | 18 +- .../[workspaceSlug]/workspace-views/index.tsx | 12 +- web/pages/_document.tsx | 2 +- web/pages/_error.tsx | 5 +- web/pages/accounts/sign-up.tsx | 10 +- web/pages/create-workspace.tsx | 14 +- web/pages/god-mode/ai.tsx | 12 +- web/pages/god-mode/authorization.tsx | 11 +- web/pages/god-mode/email.tsx | 10 +- web/pages/god-mode/image.tsx | 10 +- web/pages/god-mode/index.tsx | 10 +- web/pages/index.tsx | 2 +- web/pages/installations/[provider]/index.tsx | 2 +- web/pages/invitations/index.tsx | 3 +- web/pages/onboarding/index.tsx | 36 +- web/pages/profile/activity.tsx | 8 +- web/pages/profile/change-password.tsx | 14 +- web/pages/profile/index.tsx | 41 +- web/pages/profile/preferences/email.tsx | 8 +- web/pages/profile/preferences/theme.tsx | 14 +- web/pages/workspace-invitations/index.tsx | 10 +- web/services/ai.service.ts | 2 +- web/services/analytics.service.ts | 2 +- web/services/api_token.service.ts | 2 +- web/services/app_config.service.ts | 2 +- web/services/app_installation.service.ts | 2 +- web/services/auth.service.ts | 2 +- web/services/cycle.service.ts | 2 +- web/services/dashboard.service.ts | 2 +- web/services/file.service.ts | 4 +- web/services/inbox.service.ts | 2 +- web/services/inbox/inbox-issue.service.ts | 2 +- web/services/inbox/inbox.service.ts | 2 +- web/services/instance.service.ts | 2 +- web/services/integrations/github.service.ts | 2 +- .../integrations/integration.service.ts | 2 +- web/services/integrations/jira.service.ts | 2 +- web/services/issue/issue.service.ts | 2 +- web/services/issue/issue_activity.service.ts | 2 +- web/services/issue/issue_archive.service.ts | 2 +- .../issue/issue_attachment.service.ts | 2 +- web/services/issue/issue_comment.service.ts | 2 +- web/services/issue/issue_draft.service.ts | 2 +- web/services/issue_filter.service.ts | 2 +- web/services/module.service.ts | 2 +- web/services/notification.service.ts | 2 +- .../project/project-estimate.service.ts | 2 +- .../project/project-export.service.ts | 2 +- web/services/project/project-state.service.ts | 2 +- web/services/user.service.ts | 2 +- web/services/view.service.ts | 2 +- web/services/webhook.service.ts | 2 +- web/services/workspace.service.ts | 2 +- web/store/application/app-config.store.ts | 2 +- .../application/command-palette.store.ts | 4 +- web/store/application/index.ts | 2 +- web/store/application/instance.store.ts | 2 +- web/store/cycle.store.ts | 12 +- web/store/dashboard.store.ts | 2 +- web/store/estimate.store.ts | 4 +- web/store/event-tracker.store.ts | 2 +- web/store/global-view.store.ts | 2 +- web/store/inbox/inbox.store.ts | 8 +- web/store/inbox/inbox_filter.store.ts | 4 +- web/store/inbox/inbox_issue.store.ts | 10 +- web/store/inbox/root.store.ts | 2 +- web/store/issue/archived/filter.store.ts | 20 +- web/store/issue/archived/issue.store.ts | 8 +- web/store/issue/cycle/filter.store.ts | 20 +- web/store/issue/cycle/issue.store.ts | 12 +- web/store/issue/draft/filter.store.ts | 20 +- web/store/issue/draft/issue.store.ts | 12 +- .../helpers/issue-filter-helper.store.ts | 8 +- web/store/issue/helpers/issue-helper.store.ts | 6 +- .../issue/issue-details/activity.store.ts | 8 +- .../issue/issue-details/attachment.store.ts | 10 +- .../issue/issue-details/comment.store.ts | 10 +- .../issue-details/comment_reaction.store.ts | 12 +- web/store/issue/issue-details/issue.store.ts | 2 +- web/store/issue/issue-details/link.store.ts | 4 +- .../issue/issue-details/reaction.store.ts | 12 +- .../issue/issue-details/relation.store.ts | 4 +- web/store/issue/issue-details/root.store.ts | 30 +- .../issue/issue-details/sub_issues.store.ts | 8 +- .../issue/issue-details/subscription.store.ts | 2 +- web/store/issue/issue.store.ts | 4 +- web/store/issue/issue_calendar_view.store.ts | 2 +- web/store/issue/issue_gantt_view.store.ts | 2 +- web/store/issue/module/filter.store.ts | 20 +- web/store/issue/module/issue.store.ts | 10 +- web/store/issue/profile/filter.store.ts | 20 +- web/store/issue/profile/issue.store.ts | 8 +- web/store/issue/project-views/filter.store.ts | 20 +- web/store/issue/project-views/issue.store.ts | 8 +- web/store/issue/project/filter.store.ts | 21 +- web/store/issue/project/issue.store.ts | 10 +- web/store/issue/root.store.ts | 22 +- web/store/issue/workspace/filter.store.ts | 20 +- web/store/issue/workspace/issue.store.ts | 10 +- web/store/label.store.ts | 6 +- web/store/member/index.ts | 2 +- web/store/member/project-member.store.ts | 10 +- web/store/member/workspace-member.store.ts | 10 +- web/store/mention.store.ts | 2 +- web/store/module.store.ts | 8 +- web/store/page.store.ts | 2 +- web/store/project-page.store.ts | 4 +- web/store/project/index.ts | 4 +- web/store/project/project-publish.store.ts | 4 +- web/store/project/project.store.ts | 12 +- web/store/root.store.ts | 24 +- web/store/state.store.ts | 10 +- web/store/user/index.ts | 2 +- web/store/user/user-membership.store.ts | 6 +- web/store/workspace/api-token.store.ts | 2 +- web/store/workspace/index.ts | 12 +- web/store/workspace/webhook.store.ts | 2 +- yarn.lock | 770 +++++------------- 790 files changed, 4155 insertions(+), 4051 deletions(-) create mode 100644 web/components/dashboard/widgets/recent-collaborators.tsx create mode 100644 web/components/profile/overview/priority-distribution.tsx create mode 100644 web/components/toast-alert/index.tsx create mode 100644 web/components/ui/graphs/marimekko-graph.tsx diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 148568d76..f40c1a244 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,7 +50,6 @@ chmod +x setup.sh docker compose -f docker-compose-local.yml up ``` - ## Missing a Feature? If a feature is missing, you can directly _request_ a new one [here](https://github.com/makeplane/plane/issues/new?assignees=&labels=feature&template=feature_request.yml&title=%F0%9F%9A%80+Feature%3A+). You also can do the same by choosing "🚀 Feature" when raising a [New Issue](https://github.com/makeplane/plane/issues/new/choose) on our GitHub Repository. diff --git a/ENV_SETUP.md b/ENV_SETUP.md index bfc300196..df05683ef 100644 --- a/ENV_SETUP.md +++ b/ENV_SETUP.md @@ -53,7 +53,6 @@ NGINX_PORT=80 NEXT_PUBLIC_DEPLOY_URL="http://localhost/spaces" ``` - ## {PROJECT_FOLDER}/apiserver/.env ​ diff --git a/README.md b/README.md index 52ccda474..6834199ff 100644 --- a/README.md +++ b/README.md @@ -44,20 +44,18 @@ Meet [Plane](https://plane.so). An open-source software development tool to mana > Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases. - - ## ⚡ Installation The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account where we offer a hosted solution for users. If you want more control over your data prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/docker-compose). -| Installation Methods | Documentation Link | -|-----------------|----------------------------------------------------------------------------------------------------------| -| Docker | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://docs.plane.so/docker-compose) | -| Kubernetes | [![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white)](https://docs.plane.so/kubernetes) | +| Installation Methods | Documentation Link | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Docker | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://docs.plane.so/docker-compose) | +| Kubernetes | [![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white)](https://docs.plane.so/kubernetes) | -`Instance admin` can configure instance settings using our [God-mode](https://docs.plane.so/instance-admin) feature. +`Instance admin` can configure instance settings using our [God-mode](https://docs.plane.so/instance-admin) feature. ## 🚀 Features @@ -74,9 +72,7 @@ If you want more control over your data prefer to self-host Plane, please refer - **Analytics**: Get insights into all your Plane data in real-time. Visualize issue data to spot trends, remove blockers, and progress your work. -- **Drive** (*coming soon*): The drive helps you share documents, images, videos, or any other files that make sense to you or your team and align on the problem/solution. - - +- **Drive** (_coming soon_): The drive helps you share documents, images, videos, or any other files that make sense to you or your team and align on the problem/solution. ## 🛠️ Contributors Quick Start @@ -101,10 +97,10 @@ Setting up local environment is extremely easy and straight forward. Follow the ./setup.sh ``` 5. Open the code on VSCode or similar equivalent IDE. -6. Review the `.env` files available in various folders. +6. Review the `.env` files available in various folders. Visit [Environment Setup](./ENV_SETUP.md) to know about various environment variables used in system. 7. Run the docker command to initiate services: - ``` + ``` docker compose -f docker-compose-local.yml up -d ``` @@ -119,6 +115,7 @@ The Plane community can be found on [GitHub Discussions](https://github.com/orgs Ask questions, report bugs, join discussions, voice ideas, make feature requests, or share your projects. ### Repo Activity + ![Plane Repo Activity](https://repobeats.axiom.co/api/embed/2523c6ed2f77c082b7908c33e2ab208981d76c39.svg "Repobeats analytics image") ## 📸 Screenshots @@ -181,20 +178,21 @@ Ask questions, report bugs, join discussions, voice ideas, make feature requests ## ⛓️ Security -If you believe you have found a security vulnerability in Plane, we encourage you to responsibly disclose this and not open a public issue. We will investigate all legitimate reports. +If you believe you have found a security vulnerability in Plane, we encourage you to responsibly disclose this and not open a public issue. We will investigate all legitimate reports. Email squawk@plane.so to disclose any security vulnerabilities. ## ❤️ Contribute -There are many ways to contribute to Plane, including: -- Submitting [bugs](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%F0%9F%90%9Bbug&projects=&template=--bug-report.yaml&title=%5Bbug%5D%3A+) and [feature requests](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%E2%9C%A8feature&projects=&template=--feature-request.yaml&title=%5Bfeature%5D%3A+) for various components. -- Reviewing [the documentation](https://docs.plane.so/) and submitting [pull requests](https://github.com/makeplane/plane), from fixing typos to adding new features. -- Speaking or writing about Plane or any other ecosystem integration and [letting us know](https://discord.com/invite/A92xrEGCge)! -- Upvoting [popular feature requests](https://github.com/makeplane/plane/issues) to show your support. +There are many ways to contribute to Plane, including: + +- Submitting [bugs](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%F0%9F%90%9Bbug&projects=&template=--bug-report.yaml&title=%5Bbug%5D%3A+) and [feature requests](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%E2%9C%A8feature&projects=&template=--feature-request.yaml&title=%5Bfeature%5D%3A+) for various components. +- Reviewing [the documentation](https://docs.plane.so/) and submitting [pull requests](https://github.com/makeplane/plane), from fixing typos to adding new features. +- Speaking or writing about Plane or any other ecosystem integration and [letting us know](https://discord.com/invite/A92xrEGCge)! +- Upvoting [popular feature requests](https://github.com/makeplane/plane/issues) to show your support. ### We couldn't have done this without you. - \ No newline at end of file + diff --git a/deploy/1-click/README.md b/deploy/1-click/README.md index 88ea66c4c..08bc35b28 100644 --- a/deploy/1-click/README.md +++ b/deploy/1-click/README.md @@ -31,11 +31,11 @@ curl -fsSL https://raw.githubusercontent.com/makeplane/plane/preview/deploy/1-cl ``` NOTE: `Preview` builds do not support ARM64/AARCH64 CPU architecture + -- - Expect this after a successful install ![Install Output](images/install.png) @@ -50,29 +50,33 @@ Plane App is available via the command `plane-app`. Running the command `plane-a ![Plane Help](images/help.png) -Basic Operations: +Basic Operations: + 1. Start Server using `plane-app start` 1. Stop Server using `plane-app stop` 1. Restart Server using `plane-app restart` Advanced Operations: + 1. Configure Plane using `plane-app --configure`. This will give you options to modify - - NGINX Port (default 80) - - Domain Name (default is the local server public IP address) - - File Upload Size (default 5MB) - - External Postgres DB Url (optional - default empty) - - External Redis URL (optional - default empty) - - AWS S3 Bucket (optional - to be configured only in case the user wants to use an S3 Bucket) + + - NGINX Port (default 80) + - Domain Name (default is the local server public IP address) + - File Upload Size (default 5MB) + - External Postgres DB Url (optional - default empty) + - External Redis URL (optional - default empty) + - AWS S3 Bucket (optional - to be configured only in case the user wants to use an S3 Bucket) 1. Upgrade Plane using `plane-app --upgrade`. This will get the latest stable version of Plane files (docker-compose.yaml, .env, and docker images) -1. Updating Plane App installer using `plane-app --update-installer` will update the `plane-app` utility. +1. Updating Plane App installer using `plane-app --update-installer` will update the `plane-app` utility. -1. Uninstall Plane using `plane-app --uninstall`. This will uninstall the Plane application from the server and all docker containers but do not remove the data stored in Postgres, Redis, and Minio. +1. Uninstall Plane using `plane-app --uninstall`. This will uninstall the Plane application from the server and all docker containers but do not remove the data stored in Postgres, Redis, and Minio. -1. Plane App can be reinstalled using `plane-app --install`. +1. Plane App can be reinstalled using `plane-app --install`. + +Application Data is stored in the mentioned folders: -Application Data is stored in the mentioned folders: 1. DB Data: /opt/plane/data/postgres 1. Redis Data: /opt/plane/data/redis -1. Minio Data: /opt/plane/data/minio \ No newline at end of file +1. Minio Data: /opt/plane/data/minio diff --git a/packages/editor/core/package.json b/packages/editor/core/package.json index fcb6b57bb..198b21b0f 100644 --- a/packages/editor/core/package.json +++ b/packages/editor/core/package.json @@ -59,8 +59,7 @@ "@types/node": "18.15.3", "@types/react": "^18.2.42", "@types/react-dom": "^18.2.17", - "eslint": "^7.32.0", - "eslint-config-next": "13.2.4", + "eslint-config-custom": "*", "postcss": "^8.4.29", "tailwind-config-custom": "*", "tsconfig": "*", diff --git a/packages/editor/document-editor/package.json b/packages/editor/document-editor/package.json index bd1f2d90f..870d5edd9 100644 --- a/packages/editor/document-editor/package.json +++ b/packages/editor/document-editor/package.json @@ -37,7 +37,6 @@ "@tiptap/extension-placeholder": "^2.1.13", "@tiptap/pm": "^2.1.13", "@tiptap/suggestion": "^2.1.13", - "eslint-config-next": "13.2.4", "lucide-react": "^0.309.0", "react-popper": "^2.3.0", "tippy.js": "^6.3.7", @@ -47,7 +46,7 @@ "@types/node": "18.15.3", "@types/react": "^18.2.42", "@types/react-dom": "^18.2.17", - "eslint": "8.36.0", + "eslint-config-custom": "*", "postcss": "^8.4.29", "tailwind-config-custom": "*", "tsconfig": "*", diff --git a/packages/editor/document-editor/src/ui/components/content-browser.tsx b/packages/editor/document-editor/src/ui/components/content-browser.tsx index 97231ea96..be70067a2 100644 --- a/packages/editor/document-editor/src/ui/components/content-browser.tsx +++ b/packages/editor/document-editor/src/ui/components/content-browser.tsx @@ -15,7 +15,7 @@ export const ContentBrowser = (props: ContentBrowserProps) => { const handleOnClick = (marking: IMarking) => { scrollSummary(editor, marking); if (setSidePeekVisible) setSidePeekVisible(false); - } + }; return (
    diff --git a/packages/editor/document-editor/src/ui/components/summary-popover.tsx b/packages/editor/document-editor/src/ui/components/summary-popover.tsx index 6ad7cad83..41056c6ad 100644 --- a/packages/editor/document-editor/src/ui/components/summary-popover.tsx +++ b/packages/editor/document-editor/src/ui/components/summary-popover.tsx @@ -33,8 +33,9 @@ export const SummaryPopover: React.FC = (props) => { )} - {as_ === "link" && {display}} + {itemAs === "link" && {display}} {children && setOpen(false)} items={children} />}
    diff --git a/space/lib/mobx/store-provider.tsx b/space/lib/mobx/store-provider.tsx index c6fde14ae..e12f2823a 100644 --- a/space/lib/mobx/store-provider.tsx +++ b/space/lib/mobx/store-provider.tsx @@ -9,10 +9,10 @@ let rootStore: RootStore = new RootStore(); export const MobxStoreContext = createContext(rootStore); const initializeStore = () => { - const _rootStore: RootStore = rootStore ?? new RootStore(); - if (typeof window === "undefined") return _rootStore; - if (!rootStore) rootStore = _rootStore; - return _rootStore; + const singletonRootStore: RootStore = rootStore ?? new RootStore(); + if (typeof window === "undefined") return singletonRootStore; + if (!rootStore) rootStore = singletonRootStore; + return singletonRootStore; }; export const MobxStoreProvider = ({ children }: any) => { diff --git a/space/package.json b/space/package.json index a1d600a60..7018cd241 100644 --- a/space/package.json +++ b/space/package.json @@ -49,9 +49,7 @@ "@types/react-dom": "^18.2.17", "@types/uuid": "^9.0.1", "@typescript-eslint/eslint-plugin": "^5.48.2", - "eslint": "8.34.0", "eslint-config-custom": "*", - "eslint-config-next": "13.2.1", "tailwind-config-custom": "*", "tsconfig": "*" } diff --git a/web/.eslintrc.js b/web/.eslintrc.js index c8df60750..eb05b2af8 100644 --- a/web/.eslintrc.js +++ b/web/.eslintrc.js @@ -1,4 +1,103 @@ module.exports = { root: true, extends: ["custom"], + parser: "@typescript-eslint/parser", + settings: { + "import/resolver": { + typescript: {}, + node: { + moduleDirectory: ["node_modules", "."], + }, + }, + }, + rules: { + // "import/order": [ + // "error", + // { + // groups: ["builtin", "external", "internal", "parent", "sibling"], + // pathGroups: [ + // { + // pattern: "react", + // group: "external", + // position: "before", + // }, + // { + // pattern: "@headlessui/**", + // group: "external", + // position: "after", + // }, + // { + // pattern: "lucide-react", + // group: "external", + // position: "after", + // }, + // { + // pattern: "@plane/ui", + // group: "external", + // position: "after", + // }, + // { + // pattern: "components/**", + // group: "internal", + // position: "before", + // }, + // { + // pattern: "constants/**", + // group: "internal", + // position: "before", + // }, + // { + // pattern: "contexts/**", + // group: "internal", + // position: "before", + // }, + // { + // pattern: "helpers/**", + // group: "internal", + // position: "before", + // }, + // { + // pattern: "hooks/**", + // group: "internal", + // position: "before", + // }, + // { + // pattern: "layouts/**", + // group: "internal", + // position: "before", + // }, + // { + // pattern: "lib/**", + // group: "internal", + // position: "before", + // }, + // { + // pattern: "services/**", + // group: "internal", + // position: "before", + // }, + // { + // pattern: "store/**", + // group: "internal", + // position: "before", + // }, + // { + // pattern: "@plane/types", + // group: "internal", + // position: "after", + // }, + // { + // pattern: "lib/types", + // group: "internal", + // position: "after", + // }, + // ], + // pathGroupsExcludedImportTypes: ["builtin", "internal", "react"], + // alphabetize: { + // order: "asc", + // caseInsensitive: true, + // }, + // }, + // ], + }, }; diff --git a/web/components/account/deactivate-account-modal.tsx b/web/components/account/deactivate-account-modal.tsx index 41d1fd7ca..34129cebe 100644 --- a/web/components/account/deactivate-account-modal.tsx +++ b/web/components/account/deactivate-account-modal.tsx @@ -1,13 +1,13 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; import { useTheme } from "next-themes"; +import { mutate } from "swr"; import { Dialog, Transition } from "@headlessui/react"; import { Trash2 } from "lucide-react"; -import { mutate } from "swr"; // hooks -import { useUser } from "hooks/store"; // ui import { Button, TOAST_TYPE, setToast } from "@plane/ui"; +import { useUser } from "hooks/store"; type Props = { isOpen: boolean; @@ -86,9 +86,9 @@ export const DeactivateAccountModal: React.FC = (props) => {
    -
    +
    diff --git a/web/components/account/o-auth/o-auth-options.tsx b/web/components/account/o-auth/o-auth-options.tsx index bbb73b855..1671b94fc 100644 --- a/web/components/account/o-auth/o-auth-options.tsx +++ b/web/components/account/o-auth/o-auth-options.tsx @@ -1,12 +1,10 @@ import { observer } from "mobx-react-lite"; // services -import { AuthService } from "services/auth.service"; -// hooks +import { TOAST_TYPE, setToast } from "@plane/ui"; +import { GitHubSignInButton, GoogleSignInButton } from "components/account"; import { useApplication } from "hooks/store"; // ui -import { TOAST_TYPE, setToast } from "@plane/ui"; // components -import { GitHubSignInButton, GoogleSignInButton } from "components/account"; type Props = { handleSignInRedirection: () => Promise; diff --git a/web/components/account/sign-in-forms/optional-set-password.tsx b/web/components/account/sign-in-forms/optional-set-password.tsx index 8fc7935cd..5555d0016 100644 --- a/web/components/account/sign-in-forms/optional-set-password.tsx +++ b/web/components/account/sign-in-forms/optional-set-password.tsx @@ -157,7 +157,7 @@ export const SignInOptionalSetPasswordForm: React.FC = (props) => {
    )} /> -

    +

    Whatever you choose now will be your account{"'"}s password until you change it.

    diff --git a/web/components/account/sign-in-forms/password.tsx b/web/components/account/sign-in-forms/password.tsx index 7d51b0cf5..f42398850 100644 --- a/web/components/account/sign-in-forms/password.tsx +++ b/web/components/account/sign-in-forms/password.tsx @@ -1,22 +1,22 @@ import React, { useState } from "react"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; import { Controller, useForm } from "react-hook-form"; import { Eye, EyeOff, XCircle } from "lucide-react"; // services +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; +import { ESignInSteps, ForgotPasswordPopover } from "components/account"; +import { FORGOT_PASSWORD, SIGN_IN_WITH_PASSWORD } from "constants/event-tracker"; +import { checkEmailValidity } from "helpers/string.helper"; +import { useApplication, useEventTracker } from "hooks/store"; import { AuthService } from "services/auth.service"; // hooks -import { useApplication, useEventTracker } from "hooks/store"; // components -import { ESignInSteps, ForgotPasswordPopover } from "components/account"; // ui -import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers -import { checkEmailValidity } from "helpers/string.helper"; // types import { IPasswordSignInData } from "@plane/types"; // constants -import { FORGOT_PASSWORD, SIGN_IN_WITH_PASSWORD } from "constants/event-tracker"; type Props = { email: string; diff --git a/web/components/account/sign-in-forms/root.tsx b/web/components/account/sign-in-forms/root.tsx index 62f63caea..835e018dc 100644 --- a/web/components/account/sign-in-forms/root.tsx +++ b/web/components/account/sign-in-forms/root.tsx @@ -1,11 +1,7 @@ import React, { useEffect, useState } from "react"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; // hooks -import { useApplication, useEventTracker } from "hooks/store"; -import useSignInRedirection from "hooks/use-sign-in-redirection"; -// components -import { LatestFeatureBlock } from "components/common"; import { SignInEmailForm, SignInUniqueCodeForm, @@ -13,8 +9,12 @@ import { OAuthOptions, SignInOptionalSetPasswordForm, } from "components/account"; -// constants +import { LatestFeatureBlock } from "components/common"; import { NAVIGATE_TO_SIGNUP } from "constants/event-tracker"; +import { useApplication, useEventTracker } from "hooks/store"; +import useSignInRedirection from "hooks/use-sign-in-redirection"; +// components +// constants export enum ESignInSteps { EMAIL = "EMAIL", diff --git a/web/components/account/sign-in-forms/unique-code.tsx b/web/components/account/sign-in-forms/unique-code.tsx index 25ee4c462..6929ef0fe 100644 --- a/web/components/account/sign-in-forms/unique-code.tsx +++ b/web/components/account/sign-in-forms/unique-code.tsx @@ -2,19 +2,21 @@ import React, { useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { XCircle } from "lucide-react"; // services +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; + +import { CODE_VERIFIED } from "constants/event-tracker"; +import { checkEmailValidity } from "helpers/string.helper"; +import { useEventTracker } from "hooks/store"; + +import useTimer from "hooks/use-timer"; import { AuthService } from "services/auth.service"; import { UserService } from "services/user.service"; // hooks -import useTimer from "hooks/use-timer"; -import { useEventTracker } from "hooks/store"; // ui -import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers -import { checkEmailValidity } from "helpers/string.helper"; // types import { IEmailCheckData, IMagicSignInData } from "@plane/types"; // constants -import { CODE_VERIFIED } from "constants/event-tracker"; type Props = { email: string; diff --git a/web/components/account/sign-up-forms/email.tsx b/web/components/account/sign-up-forms/email.tsx index b65ca95bf..22dba892f 100644 --- a/web/components/account/sign-up-forms/email.tsx +++ b/web/components/account/sign-up-forms/email.tsx @@ -1,13 +1,13 @@ import React from "react"; +import { observer } from "mobx-react-lite"; import { Controller, useForm } from "react-hook-form"; import { XCircle } from "lucide-react"; -import { observer } from "mobx-react-lite"; // services +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; +import { checkEmailValidity } from "helpers/string.helper"; import { AuthService } from "services/auth.service"; // ui -import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers -import { checkEmailValidity } from "helpers/string.helper"; // types import { IEmailCheckData } from "@plane/types"; diff --git a/web/components/account/sign-up-forms/optional-set-password.tsx b/web/components/account/sign-up-forms/optional-set-password.tsx index 651f2815f..93f774248 100644 --- a/web/components/account/sign-up-forms/optional-set-password.tsx +++ b/web/components/account/sign-up-forms/optional-set-password.tsx @@ -1,19 +1,19 @@ import React, { useState } from "react"; import { Controller, useForm } from "react-hook-form"; // services +import { Eye, EyeOff } from "lucide-react"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; +import { ESignUpSteps } from "components/account"; +import { PASSWORD_CREATE_SKIPPED, SETUP_PASSWORD } from "constants/event-tracker"; +import { checkEmailValidity } from "helpers/string.helper"; +import { useEventTracker } from "hooks/store"; import { AuthService } from "services/auth.service"; // hooks -import { useEventTracker } from "hooks/store"; // ui -import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers -import { checkEmailValidity } from "helpers/string.helper"; // components -import { ESignUpSteps } from "components/account"; // constants -import { PASSWORD_CREATE_SELECTED, PASSWORD_CREATE_SKIPPED, SETUP_PASSWORD } from "constants/event-tracker"; // icons -import { Eye, EyeOff } from "lucide-react"; type Props = { email: string; @@ -162,7 +162,7 @@ export const SignUpOptionalSetPasswordForm: React.FC = (props) => {
    )} /> -

    +

    This password will continue to be your account{"'"}s password.

    diff --git a/web/components/account/sign-up-forms/password.tsx b/web/components/account/sign-up-forms/password.tsx index 5207a5024..7fab81fbe 100644 --- a/web/components/account/sign-up-forms/password.tsx +++ b/web/components/account/sign-up-forms/password.tsx @@ -1,14 +1,14 @@ import React, { useState } from "react"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; import { Controller, useForm } from "react-hook-form"; import { Eye, EyeOff, XCircle } from "lucide-react"; // services -import { AuthService } from "services/auth.service"; // ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; +import { AuthService } from "services/auth.service"; // types import { IPasswordSignInData } from "@plane/types"; @@ -134,7 +134,7 @@ export const SignUpPasswordForm: React.FC = observer((props) => {
    )} /> -

    +

    This password will continue to be your account{"'"}s password.

    diff --git a/web/components/account/sign-up-forms/root.tsx b/web/components/account/sign-up-forms/root.tsx index 8eeb5e99f..455112e9e 100644 --- a/web/components/account/sign-up-forms/root.tsx +++ b/web/components/account/sign-up-forms/root.tsx @@ -1,9 +1,7 @@ import React, { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; // hooks -import { useApplication, useEventTracker } from "hooks/store"; -import useSignInRedirection from "hooks/use-sign-in-redirection"; -// components +import Link from "next/link"; import { OAuthOptions, SignUpEmailForm, @@ -11,9 +9,11 @@ import { SignUpPasswordForm, SignUpUniqueCodeForm, } from "components/account"; -import Link from "next/link"; -// constants import { NAVIGATE_TO_SIGNIN } from "constants/event-tracker"; +import { useApplication, useEventTracker } from "hooks/store"; +import useSignInRedirection from "hooks/use-sign-in-redirection"; +// components +// constants export enum ESignUpSteps { EMAIL = "EMAIL", diff --git a/web/components/account/sign-up-forms/unique-code.tsx b/web/components/account/sign-up-forms/unique-code.tsx index 51705ea67..28581aed4 100644 --- a/web/components/account/sign-up-forms/unique-code.tsx +++ b/web/components/account/sign-up-forms/unique-code.tsx @@ -3,19 +3,20 @@ import Link from "next/link"; import { Controller, useForm } from "react-hook-form"; import { XCircle } from "lucide-react"; // services +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; + +import { CODE_VERIFIED } from "constants/event-tracker"; +import { checkEmailValidity } from "helpers/string.helper"; +import { useEventTracker } from "hooks/store"; +import useTimer from "hooks/use-timer"; import { AuthService } from "services/auth.service"; import { UserService } from "services/user.service"; // hooks -import useTimer from "hooks/use-timer"; -import { useEventTracker } from "hooks/store"; // ui -import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers -import { checkEmailValidity } from "helpers/string.helper"; // types import { IEmailCheckData, IMagicSignInData } from "@plane/types"; // constants -import { CODE_VERIFIED } from "constants/event-tracker"; type Props = { email: string; diff --git a/web/components/analytics/custom-analytics/custom-analytics.tsx b/web/components/analytics/custom-analytics/custom-analytics.tsx index 0c3ec8925..1159689c6 100644 --- a/web/components/analytics/custom-analytics/custom-analytics.tsx +++ b/web/components/analytics/custom-analytics/custom-analytics.tsx @@ -1,17 +1,17 @@ -import { useRouter } from "next/router"; -import useSWR from "swr"; -import { useForm } from "react-hook-form"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; +import { useForm } from "react-hook-form"; +import useSWR from "swr"; // services -import { AnalyticsService } from "services/analytics.service"; // components import { CustomAnalyticsSelectBar, CustomAnalyticsMainContent, CustomAnalyticsSidebar } from "components/analytics"; // types -import { IAnalyticsParams } from "@plane/types"; // fetch-keys import { ANALYTICS } from "constants/fetch-keys"; import { cn } from "helpers/common.helper"; import { useApplication } from "hooks/store"; +import { AnalyticsService } from "services/analytics.service"; +import { IAnalyticsParams } from "@plane/types"; type Props = { additionalParams?: Partial; diff --git a/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx b/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx index ec7c40195..b90e9994f 100644 --- a/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx +++ b/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx @@ -60,8 +60,8 @@ export const CustomTooltip: React.FC = ({ datum, analytics, params }) => ? "capitalize" : "" : params.x_axis === "priority" || params.x_axis === "state__group" - ? "capitalize" - : "" + ? "capitalize" + : "" }`} > {params.segment === "assignees__id" ? renderAssigneeName(tooltipValue.toString()) : tooltipValue}: diff --git a/web/components/analytics/custom-analytics/graph/index.tsx b/web/components/analytics/custom-analytics/graph/index.tsx index 51b4089c4..0e70fd898 100644 --- a/web/components/analytics/custom-analytics/graph/index.tsx +++ b/web/components/analytics/custom-analytics/graph/index.tsx @@ -1,15 +1,15 @@ // nivo import { BarDatum } from "@nivo/bar"; // components -import { CustomTooltip } from "./custom-tooltip"; import { Tooltip } from "@plane/ui"; // ui import { BarGraph } from "components/ui"; // helpers -import { findStringWithMostCharacters } from "helpers/array.helper"; import { generateBarColor, generateDisplayName } from "helpers/analytics.helper"; +import { findStringWithMostCharacters } from "helpers/array.helper"; // types import { IAnalyticsParams, IAnalyticsResponse } from "@plane/types"; +import { CustomTooltip } from "./custom-tooltip"; type Props = { analytics: IAnalyticsResponse; @@ -101,8 +101,8 @@ export const AnalyticsGraph: React.FC = ({ analytics, barGraphData, param ? generateDisplayName(datum.value, analytics, params, "x_axis")[0].toUpperCase() : "?" : datum.value && datum.value !== "None" - ? `${datum.value}`.toUpperCase()[0] - : "?"} + ? `${datum.value}`.toUpperCase()[0] + : "?"} diff --git a/web/components/analytics/custom-analytics/main-content.tsx b/web/components/analytics/custom-analytics/main-content.tsx index 7781e7869..e13b9cdd1 100644 --- a/web/components/analytics/custom-analytics/main-content.tsx +++ b/web/components/analytics/custom-analytics/main-content.tsx @@ -2,15 +2,15 @@ import { useRouter } from "next/router"; import { mutate } from "swr"; // components +import { Button, Loader } from "@plane/ui"; import { AnalyticsGraph, AnalyticsTable } from "components/analytics"; // ui -import { Button, Loader } from "@plane/ui"; // helpers +import { ANALYTICS } from "constants/fetch-keys"; import { convertResponseToBarGraphData } from "helpers/analytics.helper"; // types import { IAnalyticsParams, IAnalyticsResponse } from "@plane/types"; // fetch-keys -import { ANALYTICS } from "constants/fetch-keys"; type Props = { analytics: IAnalyticsResponse | undefined; diff --git a/web/components/analytics/custom-analytics/select-bar.tsx b/web/components/analytics/custom-analytics/select-bar.tsx index 31acb8471..7ce2f31ef 100644 --- a/web/components/analytics/custom-analytics/select-bar.tsx +++ b/web/components/analytics/custom-analytics/select-bar.tsx @@ -1,9 +1,9 @@ import { observer } from "mobx-react-lite"; import { Control, Controller, UseFormSetValue } from "react-hook-form"; // hooks +import { SelectProject, SelectSegment, SelectXAxis, SelectYAxis } from "components/analytics"; import { useProject } from "hooks/store"; // components -import { SelectProject, SelectSegment, SelectXAxis, SelectYAxis } from "components/analytics"; // types import { IAnalyticsParams } from "@plane/types"; @@ -22,8 +22,9 @@ export const CustomAnalyticsSelectBar: React.FC = observer((props) => { return (
    {!isProjectLevel && (
    diff --git a/web/components/analytics/custom-analytics/select/project.tsx b/web/components/analytics/custom-analytics/select/project.tsx index 3c08e1574..61c3acb09 100644 --- a/web/components/analytics/custom-analytics/select/project.tsx +++ b/web/components/analytics/custom-analytics/select/project.tsx @@ -1,8 +1,8 @@ import { observer } from "mobx-react-lite"; // hooks +import { CustomSearchSelect } from "@plane/ui"; import { useProject } from "hooks/store"; // ui -import { CustomSearchSelect } from "@plane/ui"; type Props = { value: string[] | undefined; diff --git a/web/components/analytics/custom-analytics/select/segment.tsx b/web/components/analytics/custom-analytics/select/segment.tsx index 055665d9e..de94eac62 100644 --- a/web/components/analytics/custom-analytics/select/segment.tsx +++ b/web/components/analytics/custom-analytics/select/segment.tsx @@ -3,9 +3,9 @@ import { useRouter } from "next/router"; // ui import { CustomSelect } from "@plane/ui"; // types +import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics"; import { IAnalyticsParams, TXAxisValues } from "@plane/types"; // constants -import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics"; type Props = { value: TXAxisValues | null | undefined; diff --git a/web/components/analytics/custom-analytics/select/x-axis.tsx b/web/components/analytics/custom-analytics/select/x-axis.tsx index 74ee99a77..9daecaaa0 100644 --- a/web/components/analytics/custom-analytics/select/x-axis.tsx +++ b/web/components/analytics/custom-analytics/select/x-axis.tsx @@ -3,9 +3,9 @@ import { useRouter } from "next/router"; // ui import { CustomSelect } from "@plane/ui"; // types +import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics"; import { IAnalyticsParams, TXAxisValues } from "@plane/types"; // constants -import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics"; type Props = { value: TXAxisValues; diff --git a/web/components/analytics/custom-analytics/select/y-axis.tsx b/web/components/analytics/custom-analytics/select/y-axis.tsx index 9f66c6b54..92e4fd2e5 100644 --- a/web/components/analytics/custom-analytics/select/y-axis.tsx +++ b/web/components/analytics/custom-analytics/select/y-axis.tsx @@ -1,9 +1,9 @@ // ui import { CustomSelect } from "@plane/ui"; // types +import { ANALYTICS_Y_AXIS_VALUES } from "constants/analytics"; import { TYAxisValues } from "@plane/types"; // constants -import { ANALYTICS_Y_AXIS_VALUES } from "constants/analytics"; type Props = { value: TYAxisValues; diff --git a/web/components/analytics/custom-analytics/sidebar/projects-list.tsx b/web/components/analytics/custom-analytics/sidebar/projects-list.tsx index 334e9160f..9a0eec227 100644 --- a/web/components/analytics/custom-analytics/sidebar/projects-list.tsx +++ b/web/components/analytics/custom-analytics/sidebar/projects-list.tsx @@ -1,11 +1,11 @@ import { observer } from "mobx-react-lite"; // hooks -import { useProject } from "hooks/store"; // icons import { Contrast, LayoutGrid, Users } from "lucide-react"; // helpers import { renderEmoji } from "helpers/emoji.helper"; import { truncateText } from "helpers/string.helper"; +import { useProject } from "hooks/store"; type Props = { projectIds: string[]; diff --git a/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx b/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx index 6a7b3c7b9..fb9ab90fa 100644 --- a/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx +++ b/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx @@ -1,12 +1,12 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { NETWORK_CHOICES } from "constants/project"; +import { renderFormattedDate } from "helpers/date-time.helper"; +import { renderEmoji } from "helpers/emoji.helper"; import { useCycle, useMember, useModule, useProject } from "hooks/store"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; -import { renderFormattedDate } from "helpers/date-time.helper"; // constants -import { NETWORK_CHOICES } from "constants/project"; export const CustomAnalyticsSidebarHeader = observer(() => { const router = useRouter(); diff --git a/web/components/analytics/custom-analytics/sidebar/sidebar.tsx b/web/components/analytics/custom-analytics/sidebar/sidebar.tsx index aab7f874f..7a7c52377 100644 --- a/web/components/analytics/custom-analytics/sidebar/sidebar.tsx +++ b/web/components/analytics/custom-analytics/sidebar/sidebar.tsx @@ -1,24 +1,24 @@ import { useEffect } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { mutate } from "swr"; // services -import { AnalyticsService } from "services/analytics.service"; // hooks -import { useCycle, useModule, useProject, useUser, useWorkspace } from "hooks/store"; // components -import { CustomAnalyticsSidebarHeader, CustomAnalyticsSidebarProjectsList } from "components/analytics"; // ui +import { CalendarDays, Download, RefreshCw } from "lucide-react"; import { Button, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui"; // icons -import { CalendarDays, Download, RefreshCw } from "lucide-react"; +import { CustomAnalyticsSidebarHeader, CustomAnalyticsSidebarProjectsList } from "components/analytics"; // helpers -import { renderFormattedDate } from "helpers/date-time.helper"; // types -import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData, IWorkspace } from "@plane/types"; // fetch-keys import { ANALYTICS } from "constants/fetch-keys"; import { cn } from "helpers/common.helper"; +import { renderFormattedDate } from "helpers/date-time.helper"; +import { useCycle, useModule, useProject, useUser, useWorkspace } from "hooks/store"; +import { AnalyticsService } from "services/analytics.service"; +import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData, IWorkspace } from "@plane/types"; type Props = { analytics: IAnalyticsResponse | undefined; @@ -143,7 +143,7 @@ export const CustomAnalyticsSidebar: React.FC = observer((props) => { return (
    @@ -176,10 +176,10 @@ export const CustomAnalyticsSidebar: React.FC = observer((props) => {
    -
    +
    diff --git a/web/components/core/modals/user-image-upload-modal.tsx b/web/components/core/modals/user-image-upload-modal.tsx index 3d0fbb9ee..7f41b8225 100644 --- a/web/components/core/modals/user-image-upload-modal.tsx +++ b/web/components/core/modals/user-image-upload-modal.tsx @@ -3,15 +3,16 @@ import { observer } from "mobx-react-lite"; import { useDropzone } from "react-dropzone"; import { Transition, Dialog } from "@headlessui/react"; // hooks +import { UserCircle2 } from "lucide-react"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; + +import { MAX_FILE_SIZE } from "constants/common"; import { useApplication } from "hooks/store"; // services import { FileService } from "services/file.service"; // ui -import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // icons -import { UserCircle2 } from "lucide-react"; // constants -import { MAX_FILE_SIZE } from "constants/common"; type Props = { handleDelete?: () => void; diff --git a/web/components/core/modals/workspace-image-upload-modal.tsx b/web/components/core/modals/workspace-image-upload-modal.tsx index eec62b919..9c1a8363b 100644 --- a/web/components/core/modals/workspace-image-upload-modal.tsx +++ b/web/components/core/modals/workspace-image-upload-modal.tsx @@ -1,18 +1,18 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { useDropzone } from "react-dropzone"; import { Transition, Dialog } from "@headlessui/react"; // hooks +import { UserCircle2 } from "lucide-react"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; +import { MAX_FILE_SIZE } from "constants/common"; import { useApplication, useWorkspace } from "hooks/store"; // services import { FileService } from "services/file.service"; // ui -import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // icons -import { UserCircle2 } from "lucide-react"; // constants -import { MAX_FILE_SIZE } from "constants/common"; type Props = { handleRemove?: () => void; diff --git a/web/components/core/render-if-visible-HOC.tsx b/web/components/core/render-if-visible-HOC.tsx index 24ae19fe7..f0e9f59b4 100644 --- a/web/components/core/render-if-visible-HOC.tsx +++ b/web/components/core/render-if-visible-HOC.tsx @@ -1,10 +1,10 @@ -import { cn } from "helpers/common.helper"; import React, { useState, useRef, useEffect, ReactNode, MutableRefObject } from "react"; +import { cn } from "helpers/common.helper"; type Props = { defaultHeight?: string; verticalOffset?: number; - horizonatlOffset?: number; + horizontalOffset?: number; root?: MutableRefObject; children: ReactNode; as?: keyof JSX.IntrinsicElements; @@ -20,7 +20,7 @@ const RenderIfVisible: React.FC = (props) => { defaultHeight = "300px", root, verticalOffset = 50, - horizonatlOffset = 0, + horizontalOffset = 0, as = "div", children, classNames = "", @@ -52,17 +52,18 @@ const RenderIfVisible: React.FC = (props) => { }, { root: root?.current, - rootMargin: `${verticalOffset}% ${horizonatlOffset}% ${verticalOffset}% ${horizonatlOffset}%`, + rootMargin: `${verticalOffset}% ${horizontalOffset}% ${verticalOffset}% ${horizontalOffset}%`, } ); observer.observe(intersectionRef.current); return () => { if (intersectionRef.current) { + // eslint-disable-next-line react-hooks/exhaustive-deps observer.unobserve(intersectionRef.current); } }; } - }, [root?.current, intersectionRef, children, changingReference]); + }, [intersectionRef, children, changingReference, root, verticalOffset, horizontalOffset]); //Set height after render useEffect(() => { diff --git a/web/components/core/sidebar/links-list.tsx b/web/components/core/sidebar/links-list.tsx index 6b987e308..3e068e4f0 100644 --- a/web/components/core/sidebar/links-list.tsx +++ b/web/components/core/sidebar/links-list.tsx @@ -1,15 +1,14 @@ -// ui -import { ExternalLinkIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; +import { observer } from "mobx-react"; // icons import { Pencil, Trash2, LinkIcon } from "lucide-react"; +// ui +import { ExternalLinkIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { calculateTimeAgo } from "helpers/date-time.helper"; +// hooks +import { useMember } from "hooks/store"; // types import { ILinkDetails, UserAuth } from "@plane/types"; -// hooks -import { observer } from "mobx-react"; -import { useMeasure } from "@nivo/core"; -import { useMember } from "hooks/store"; type Props = { links: ILinkDetails[]; diff --git a/web/components/core/sidebar/sidebar-menu-hamburger-toggle.tsx b/web/components/core/sidebar/sidebar-menu-hamburger-toggle.tsx index 0212e4980..880cf8146 100644 --- a/web/components/core/sidebar/sidebar-menu-hamburger-toggle.tsx +++ b/web/components/core/sidebar/sidebar-menu-hamburger-toggle.tsx @@ -1,7 +1,7 @@ import { FC } from "react"; +import { observer } from "mobx-react"; import { Menu } from "lucide-react"; import { useApplication } from "hooks/store"; -import { observer } from "mobx-react"; type Props = { onClick?: () => void; diff --git a/web/components/core/sidebar/sidebar-progress-stats.tsx b/web/components/core/sidebar/sidebar-progress-stats.tsx index 12c387f47..157fd2c79 100644 --- a/web/components/core/sidebar/sidebar-progress-stats.tsx +++ b/web/components/core/sidebar/sidebar-progress-stats.tsx @@ -4,14 +4,14 @@ import Image from "next/image"; // headless ui import { Tab } from "@headlessui/react"; // hooks +import { Avatar, StateGroupIcon } from "@plane/ui"; +import { SingleProgressStats } from "components/core"; import useLocalStorage from "hooks/use-local-storage"; // images import emptyLabel from "public/empty-state/empty_label.svg"; import emptyMembers from "public/empty-state/empty_members.svg"; // components -import { SingleProgressStats } from "components/core"; // ui -import { Avatar, StateGroupIcon } from "@plane/ui"; // types import { IModule, diff --git a/web/components/core/theme/color-picker-input.tsx b/web/components/core/theme/color-picker-input.tsx index 19cd519cb..03ac06eae 100644 --- a/web/components/core/theme/color-picker-input.tsx +++ b/web/components/core/theme/color-picker-input.tsx @@ -1,5 +1,6 @@ import { FC, Fragment } from "react"; // react-form +import { ColorResult, SketchPicker } from "react-color"; import { Control, Controller, @@ -11,12 +12,11 @@ import { UseFormWatch, } from "react-hook-form"; // react-color -import { ColorResult, SketchPicker } from "react-color"; // component import { Popover, Transition } from "@headlessui/react"; +import { Palette } from "lucide-react"; import { Input } from "@plane/ui"; // icons -import { Palette } from "lucide-react"; // types import { IUserTheme } from "@plane/types"; diff --git a/web/components/core/theme/custom-theme-selector.tsx b/web/components/core/theme/custom-theme-selector.tsx index fdb7a6483..b9e94a2d2 100644 --- a/web/components/core/theme/custom-theme-selector.tsx +++ b/web/components/core/theme/custom-theme-selector.tsx @@ -1,10 +1,10 @@ import { observer } from "mobx-react-lite"; -import { Controller, useForm } from "react-hook-form"; import { useTheme } from "next-themes"; +import { Controller, useForm } from "react-hook-form"; // hooks +import { Button, InputColorPicker } from "@plane/ui"; import { useUser } from "hooks/store"; // ui -import { Button, InputColorPicker } from "@plane/ui"; // types import { IUserTheme } from "@plane/types"; diff --git a/web/components/core/theme/theme-switch.tsx b/web/components/core/theme/theme-switch.tsx index bcd847a28..428e6930b 100644 --- a/web/components/core/theme/theme-switch.tsx +++ b/web/components/core/theme/theme-switch.tsx @@ -1,8 +1,8 @@ import { FC } from "react"; // constants +import { CustomSelect } from "@plane/ui"; import { THEME_OPTIONS, I_THEME_OPTION } from "constants/themes"; // ui -import { CustomSelect } from "@plane/ui"; type Props = { value: I_THEME_OPTION | null; diff --git a/web/components/cycles/active-cycle-details.tsx b/web/components/cycles/active-cycle-details.tsx index bc22cb8ab..d9309d4b5 100644 --- a/web/components/cycles/active-cycle-details.tsx +++ b/web/components/cycles/active-cycle-details.tsx @@ -1,8 +1,8 @@ import { MouseEvent } from "react"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; -import useSWR from "swr"; +import Link from "next/link"; import { useTheme } from "next-themes"; +import useSWR from "swr"; // hooks import { useCycle, useIssues, useMember, useProject, useUser } from "hooks/store"; // ui @@ -183,7 +183,7 @@ export const ActiveCycleDetails: React.FC = observer((props - + {`${daysLeft} ${daysLeft > 1 ? "days" : "day"} left`} {activeCycle.is_favorite ? ( @@ -303,9 +303,9 @@ export const ActiveCycleDetails: React.FC = observer((props
    -
    +
    High Priority Issues
    -
    +
    {activeCycleIssues ? ( activeCycleIssues.length > 0 ? ( activeCycleIssues.map((issue: any) => ( @@ -329,17 +329,17 @@ export const ActiveCycleDetails: React.FC = observer((props {truncateText(issue.name, 30)}
    -
    +
    {}} projectId={projectId?.toString() ?? ""} - disabled={true} + disabled buttonVariant="background-with-text" /> {issue.target_date && ( -
    +
    {renderFormattedDateWithoutYear(issue.target_date)}
    @@ -349,7 +349,7 @@ export const ActiveCycleDetails: React.FC = observer((props )) ) : ( -
    +
    There are no high priority issues present in this cycle.
    ) @@ -362,7 +362,7 @@ export const ActiveCycleDetails: React.FC = observer((props )}
    -
    +
    diff --git a/web/components/cycles/active-cycle-stats.tsx b/web/components/cycles/active-cycle-stats.tsx index 3ca5caeb2..7d935c347 100644 --- a/web/components/cycles/active-cycle-stats.tsx +++ b/web/components/cycles/active-cycle-stats.tsx @@ -1,11 +1,11 @@ import React, { Fragment } from "react"; import { Tab } from "@headlessui/react"; // hooks +import { Avatar } from "@plane/ui"; +import { SingleProgressStats } from "components/core"; import useLocalStorage from "hooks/use-local-storage"; // components -import { SingleProgressStats } from "components/core"; // ui -import { Avatar } from "@plane/ui"; // types import { ICycle } from "@plane/types"; diff --git a/web/components/cycles/cycle-mobile-header.tsx b/web/components/cycles/cycle-mobile-header.tsx index 624334ec4..942b5832b 100644 --- a/web/components/cycles/cycle-mobile-header.tsx +++ b/web/components/cycles/cycle-mobile-header.tsx @@ -1,16 +1,16 @@ import { useCallback, useState } from "react"; import router from "next/router"; //components -import { CustomMenu } from "@plane/ui"; // icons import { Calendar, ChevronDown, Kanban, List } from "lucide-react"; -import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; +import { CustomMenu } from "@plane/ui"; // hooks -import { useIssues, useCycle, useProjectState, useLabel, useMember } from "hooks/store"; // constants -import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "constants/issue"; import { ProjectAnalyticsModal } from "components/analytics"; import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues"; +import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "constants/issue"; +import { useIssues, useCycle, useProjectState, useLabel, useMember } from "hooks/store"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; export const CycleMobileHeader = () => { const [analyticsModal, setAnalyticsModal] = useState(false); @@ -100,6 +100,7 @@ export const CycleMobileHeader = () => { > {layouts.map((layout, index) => ( { handleLayoutChange(ISSUE_LAYOUTS[index].key); }} diff --git a/web/components/cycles/cycle-peek-overview.tsx b/web/components/cycles/cycle-peek-overview.tsx index fbfb46b50..b7e778c10 100644 --- a/web/components/cycles/cycle-peek-overview.tsx +++ b/web/components/cycles/cycle-peek-overview.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks import { useCycle } from "hooks/store"; // components diff --git a/web/components/cycles/cycles-board-card.tsx b/web/components/cycles/cycles-board-card.tsx index 72af8409d..2eecb1ae9 100644 --- a/web/components/cycles/cycles-board-card.tsx +++ b/web/components/cycles/cycles-board-card.tsx @@ -1,12 +1,10 @@ import { FC, MouseEvent, useState } from "react"; -import { useRouter } from "next/router"; -import Link from "next/link"; import { observer } from "mobx-react"; +import Link from "next/link"; +import { useRouter } from "next/router"; // hooks -import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; // components -import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles"; -// ui +import { Info, LinkIcon, Pencil, Star, Trash2 } from "lucide-react"; import { Avatar, AvatarGroup, @@ -18,15 +16,17 @@ import { setToast, setPromiseToast, } from "@plane/ui"; +import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles"; +// ui // icons -import { Info, LinkIcon, Pencil, Star, Trash2 } from "lucide-react"; // helpers +import { CYCLE_STATUS } from "constants/cycle"; +import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "constants/event-tracker"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { findHowManyDaysLeft, renderFormattedDate } from "helpers/date-time.helper"; import { copyTextToClipboard } from "helpers/string.helper"; // constants -import { CYCLE_STATUS } from "constants/cycle"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "constants/event-tracker"; +import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; //.types import { TCycleGroups } from "@plane/types"; diff --git a/web/components/cycles/cycles-board.tsx b/web/components/cycles/cycles-board.tsx index 1a9069267..00c98e57c 100644 --- a/web/components/cycles/cycles-board.tsx +++ b/web/components/cycles/cycles-board.tsx @@ -2,12 +2,12 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; import { useTheme } from "next-themes"; // hooks -import { useUser } from "hooks/store"; // components import { CyclePeekOverview, CyclesBoardCard } from "components/cycles"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // constants import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { useUser } from "hooks/store"; export interface ICyclesBoard { cycleIds: string[]; diff --git a/web/components/cycles/cycles-list-item.tsx b/web/components/cycles/cycles-list-item.tsx index 9ab2e3de8..9bf1866ff 100644 --- a/web/components/cycles/cycles-list-item.tsx +++ b/web/components/cycles/cycles-list-item.tsx @@ -1,12 +1,9 @@ import { FC, MouseEvent, useState } from "react"; +import { observer } from "mobx-react"; import Link from "next/link"; import { useRouter } from "next/router"; -import { observer } from "mobx-react"; // hooks -import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; -// components -import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles"; -// ui +import { Check, Info, LinkIcon, Pencil, Star, Trash2, User2 } from "lucide-react"; import { CustomMenu, Tooltip, @@ -18,17 +15,20 @@ import { setToast, setPromiseToast, } from "@plane/ui"; -// icons -import { Check, Info, LinkIcon, Pencil, Star, Trash2, User2 } from "lucide-react"; -// helpers +import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles"; +import { CYCLE_STATUS } from "constants/cycle"; +import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "constants/event-tracker"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { findHowManyDaysLeft, renderFormattedDate } from "helpers/date-time.helper"; import { copyTextToClipboard } from "helpers/string.helper"; +import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; +// components +// ui +// icons +// helpers // constants -import { CYCLE_STATUS } from "constants/cycle"; -import { EUserWorkspaceRoles } from "constants/workspace"; // types import { TCycleGroups } from "@plane/types"; -import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "constants/event-tracker"; type TCyclesListItem = { cycleId: string; @@ -227,7 +227,7 @@ export const CyclesListItem: FC = observer((props) => {
    -
    diff --git a/web/components/cycles/cycles-list.tsx b/web/components/cycles/cycles-list.tsx index 173a7f4b7..99cf1f2b1 100644 --- a/web/components/cycles/cycles-list.tsx +++ b/web/components/cycles/cycles-list.tsx @@ -2,14 +2,14 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; import { useTheme } from "next-themes"; // hooks -import { useUser } from "hooks/store"; // components +import { Loader } from "@plane/ui"; import { CyclePeekOverview, CyclesListItem } from "components/cycles"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui -import { Loader } from "@plane/ui"; // constants import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { useUser } from "hooks/store"; export interface ICyclesList { cycleIds: string[]; diff --git a/web/components/cycles/cycles-view.tsx b/web/components/cycles/cycles-view.tsx index a321be0b5..745ca1bd3 100644 --- a/web/components/cycles/cycles-view.tsx +++ b/web/components/cycles/cycles-view.tsx @@ -1,11 +1,11 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // hooks -import { useCycle } from "hooks/store"; // components import { CyclesBoard, CyclesList, CyclesListGanttChartView } from "components/cycles"; // ui components import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui"; +import { useCycle } from "hooks/store"; // types import { TCycleLayout, TCycleView } from "@plane/types"; @@ -32,10 +32,10 @@ export const CyclesView: FC = observer((props) => { filter === "completed" ? currentProjectCompletedCycleIds : filter === "draft" - ? currentProjectDraftCycleIds - : filter === "upcoming" - ? currentProjectUpcomingCycleIds - : currentProjectCycleIds; + ? currentProjectDraftCycleIds + : filter === "upcoming" + ? currentProjectUpcomingCycleIds + : currentProjectCycleIds; if (loader || !cyclesList) return ( diff --git a/web/components/cycles/delete-modal.tsx b/web/components/cycles/delete-modal.tsx index 239fe6a66..fd7b1f356 100644 --- a/web/components/cycles/delete-modal.tsx +++ b/web/components/cycles/delete-modal.tsx @@ -1,16 +1,16 @@ import { Fragment, useState } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; -import { observer } from "mobx-react-lite"; import { AlertTriangle } from "lucide-react"; // hooks +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; +import { CYCLE_DELETED } from "constants/event-tracker"; import { useEventTracker, useCycle } from "hooks/store"; // components -import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import { ICycle } from "@plane/types"; // constants -import { CYCLE_DELETED } from "constants/event-tracker"; interface ICycleDelete { cycle: ICycle; diff --git a/web/components/cycles/form.tsx b/web/components/cycles/form.tsx index 799d80438..4e2f55ef9 100644 --- a/web/components/cycles/form.tsx +++ b/web/components/cycles/form.tsx @@ -1,9 +1,9 @@ import { useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; // components +import { Button, Input, TextArea } from "@plane/ui"; import { DateRangeDropdown, ProjectDropdown } from "components/dropdowns"; // ui -import { Button, Input, TextArea } from "@plane/ui"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // types diff --git a/web/components/cycles/gantt-chart/blocks.tsx b/web/components/cycles/gantt-chart/blocks.tsx index 5d82c94a8..e9fdd50de 100644 --- a/web/components/cycles/gantt-chart/blocks.tsx +++ b/web/components/cycles/gantt-chart/blocks.tsx @@ -1,11 +1,11 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react"; +import { useRouter } from "next/router"; // hooks -import { useApplication, useCycle } from "hooks/store"; // ui import { Tooltip, ContrastIcon } from "@plane/ui"; // helpers import { renderFormattedDate } from "helpers/date-time.helper"; +import { useApplication, useCycle } from "hooks/store"; type Props = { cycleId: string; @@ -33,12 +33,12 @@ export const CycleGanttBlock: React.FC = observer((props) => { cycleStatus === "current" ? "#09a953" : cycleStatus === "upcoming" - ? "#f7ae59" - : cycleStatus === "completed" - ? "#3f76ff" - : cycleStatus === "draft" - ? "rgb(var(--color-text-200))" - : "", + ? "#f7ae59" + : cycleStatus === "completed" + ? "#3f76ff" + : cycleStatus === "draft" + ? "rgb(var(--color-text-200))" + : "", }} onClick={() => router.push(`/${workspaceSlug}/projects/${cycleDetails?.project_id}/cycles/${cycleDetails?.id}`)} > @@ -86,12 +86,12 @@ export const CycleGanttSidebarBlock: React.FC = observer((props) => { cycleStatus === "current" ? "#09a953" : cycleStatus === "upcoming" - ? "#f7ae59" - : cycleStatus === "completed" - ? "#3f76ff" - : cycleStatus === "draft" - ? "rgb(var(--color-text-200))" - : "" + ? "#f7ae59" + : cycleStatus === "completed" + ? "#3f76ff" + : cycleStatus === "draft" + ? "rgb(var(--color-text-200))" + : "" }`} />
    {cycleDetails?.name}
    diff --git a/web/components/cycles/gantt-chart/cycles-list-layout.tsx b/web/components/cycles/gantt-chart/cycles-list-layout.tsx index 646333aad..521273c51 100644 --- a/web/components/cycles/gantt-chart/cycles-list-layout.tsx +++ b/web/components/cycles/gantt-chart/cycles-list-layout.tsx @@ -1,15 +1,15 @@ import { FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { CycleGanttBlock } from "components/cycles"; +import { GanttChartRoot, IBlockUpdateData, CycleGanttSidebar } from "components/gantt-chart"; +import { EUserProjectRoles } from "constants/project"; import { useCycle, useUser } from "hooks/store"; // components -import { GanttChartRoot, IBlockUpdateData, CycleGanttSidebar } from "components/gantt-chart"; -import { CycleGanttBlock } from "components/cycles"; // types import { ICycle } from "@plane/types"; // constants -import { EUserProjectRoles } from "constants/project"; type Props = { workspaceSlug: string; diff --git a/web/components/cycles/modal.tsx b/web/components/cycles/modal.tsx index 1d60f1dc4..2d1640ec9 100644 --- a/web/components/cycles/modal.tsx +++ b/web/components/cycles/modal.tsx @@ -1,18 +1,18 @@ import React, { useEffect, useState } from "react"; import { Dialog, Transition } from "@headlessui/react"; // services -import { CycleService } from "services/cycle.service"; -// hooks +import { TOAST_TYPE, setToast } from "@plane/ui"; +import { CycleForm } from "components/cycles"; +import { CYCLE_CREATED, CYCLE_UPDATED } from "constants/event-tracker"; import { useEventTracker, useCycle, useProject } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; +import { CycleService } from "services/cycle.service"; +// hooks // components -import { CycleForm } from "components/cycles"; // ui -import { TOAST_TYPE, setToast } from "@plane/ui"; // types import type { CycleDateCheckData, ICycle, TCycleView } from "@plane/types"; // constants -import { CYCLE_CREATED, CYCLE_UPDATED } from "constants/event-tracker"; type CycleModalProps = { isOpen: boolean; diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index f01c840f1..06db83e0d 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -1,32 +1,31 @@ import React, { useEffect, useState } from "react"; -import { useRouter } from "next/router"; +import isEmpty from "lodash/isEmpty"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; import { Disclosure, Transition } from "@headlessui/react"; -import isEmpty from "lodash/isEmpty"; -// services -import { CycleService } from "services/cycle.service"; -// hooks -import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; +// icons +import { ChevronDown, LinkIcon, Trash2, UserCircle2, AlertCircle, ChevronRight, CalendarClock } from "lucide-react"; +// ui +import { Avatar, CustomMenu, Loader, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui"; // components import { SidebarProgressStats } from "components/core"; import ProgressChart from "components/core/sidebar/progress-chart"; import { CycleDeleteModal } from "components/cycles/delete-modal"; -// ui -import { Avatar, CustomMenu, Loader, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui"; -// icons -import { ChevronDown, LinkIcon, Trash2, UserCircle2, AlertCircle, ChevronRight, CalendarClock } from "lucide-react"; +import { DateRangeDropdown } from "components/dropdowns"; +// constants +import { CYCLE_STATUS } from "constants/cycle"; +import { CYCLE_UPDATED } from "constants/event-tracker"; +import { EUserWorkspaceRoles } from "constants/workspace"; // helpers -import { copyUrlToClipboard } from "helpers/string.helper"; import { findHowManyDaysLeft, renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { copyUrlToClipboard } from "helpers/string.helper"; +// hooks +import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; +// services +import { CycleService } from "services/cycle.service"; // types import { ICycle } from "@plane/types"; -// constants -import { EUserWorkspaceRoles } from "constants/workspace"; -import { CYCLE_UPDATED } from "constants/event-tracker"; -// fetch-keys -import { CYCLE_STATUS } from "constants/cycle"; -import { DateRangeDropdown } from "components/dropdowns"; type Props = { cycleId: string; @@ -299,7 +298,7 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { Date range
    -
    +
    void; diff --git a/web/components/dashboard/home-dashboard-widgets.tsx b/web/components/dashboard/home-dashboard-widgets.tsx index 2e2f9ef88..ab96ef90f 100644 --- a/web/components/dashboard/home-dashboard-widgets.tsx +++ b/web/components/dashboard/home-dashboard-widgets.tsx @@ -1,7 +1,5 @@ import { observer } from "mobx-react-lite"; // hooks -import { useApplication, useDashboard } from "hooks/store"; -// components import { AssignedIssuesWidget, CreatedIssuesWidget, @@ -13,6 +11,8 @@ import { RecentProjectsWidget, WidgetProps, } from "components/dashboard"; +import { useApplication, useDashboard } from "hooks/store"; +// components // types import { TWidgetKeys } from "@plane/types"; diff --git a/web/components/dashboard/project-empty-state.tsx b/web/components/dashboard/project-empty-state.tsx index bb7f82f34..32236e233 100644 --- a/web/components/dashboard/project-empty-state.tsx +++ b/web/components/dashboard/project-empty-state.tsx @@ -1,13 +1,13 @@ -import Image from "next/image"; import { observer } from "mobx-react-lite"; +import Image from "next/image"; // hooks +import { Button } from "@plane/ui"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { useApplication, useEventTracker, useUser } from "hooks/store"; // ui -import { Button } from "@plane/ui"; // assets import ProjectEmptyStateImage from "public/empty-state/dashboard/project.svg"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; export const DashboardProjectEmptyState = observer(() => { // store hooks diff --git a/web/components/dashboard/widgets/assigned-issues.tsx b/web/components/dashboard/widgets/assigned-issues.tsx index 407ac9ddf..3833d319c 100644 --- a/web/components/dashboard/widgets/assigned-issues.tsx +++ b/web/components/dashboard/widgets/assigned-issues.tsx @@ -1,10 +1,8 @@ import { useEffect, useState } from "react"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; import { Tab } from "@headlessui/react"; // hooks -import { useDashboard } from "hooks/store"; -// components import { DurationFilterDropdown, TabsList, @@ -12,12 +10,14 @@ import { WidgetLoader, WidgetProps, } from "components/dashboard/widgets"; -// helpers +import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; import { getCustomDates, getRedirectionFilters, getTabKey } from "helpers/dashboard.helper"; +import { useDashboard } from "hooks/store"; +// components +// helpers // types import { EDurationFilters, TAssignedIssuesWidgetFilters, TAssignedIssuesWidgetResponse } from "@plane/types"; // constants -import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; const WIDGET_KEY = "assigned_issues"; diff --git a/web/components/dashboard/widgets/created-issues.tsx b/web/components/dashboard/widgets/created-issues.tsx index 23e7bee27..61a1181e9 100644 --- a/web/components/dashboard/widgets/created-issues.tsx +++ b/web/components/dashboard/widgets/created-issues.tsx @@ -1,10 +1,8 @@ import { useEffect, useState } from "react"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; import { Tab } from "@headlessui/react"; // hooks -import { useDashboard } from "hooks/store"; -// components import { DurationFilterDropdown, TabsList, @@ -12,12 +10,14 @@ import { WidgetLoader, WidgetProps, } from "components/dashboard/widgets"; -// helpers +import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; import { getCustomDates, getRedirectionFilters, getTabKey } from "helpers/dashboard.helper"; +import { useDashboard } from "hooks/store"; +// components +// helpers // types import { EDurationFilters, TCreatedIssuesWidgetFilters, TCreatedIssuesWidgetResponse } from "@plane/types"; // constants -import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; const WIDGET_KEY = "created_issues"; diff --git a/web/components/dashboard/widgets/dropdowns/duration-filter.tsx b/web/components/dashboard/widgets/dropdowns/duration-filter.tsx index fbdac4f00..3cf22c350 100644 --- a/web/components/dashboard/widgets/dropdowns/duration-filter.tsx +++ b/web/components/dashboard/widgets/dropdowns/duration-filter.tsx @@ -1,15 +1,14 @@ import { useState } from "react"; import { ChevronDown } from "lucide-react"; // components +import { CustomMenu } from "@plane/ui"; import { DateFilterModal } from "components/core"; // ui -import { CustomMenu } from "@plane/ui"; // helpers import { getDurationFilterDropdownLabel } from "helpers/dashboard.helper"; // types import { EDurationFilters } from "@plane/types"; // constants -import { DURATION_FILTER_OPTIONS } from "constants/dashboard"; type Props = { customDates?: string[]; diff --git a/web/components/dashboard/widgets/empty-states/assigned-issues.tsx b/web/components/dashboard/widgets/empty-states/assigned-issues.tsx index f60d8efe6..0cfad7dc9 100644 --- a/web/components/dashboard/widgets/empty-states/assigned-issues.tsx +++ b/web/components/dashboard/widgets/empty-states/assigned-issues.tsx @@ -1,9 +1,9 @@ import Image from "next/image"; import { useTheme } from "next-themes"; // types +import { ASSIGNED_ISSUES_EMPTY_STATES } from "constants/dashboard"; import { TIssuesListTypes } from "@plane/types"; // constants -import { ASSIGNED_ISSUES_EMPTY_STATES } from "constants/dashboard"; type Props = { type: TIssuesListTypes; diff --git a/web/components/dashboard/widgets/empty-states/created-issues.tsx b/web/components/dashboard/widgets/empty-states/created-issues.tsx index fe93d4404..2c59342fc 100644 --- a/web/components/dashboard/widgets/empty-states/created-issues.tsx +++ b/web/components/dashboard/widgets/empty-states/created-issues.tsx @@ -1,9 +1,9 @@ import Image from "next/image"; import { useTheme } from "next-themes"; // types +import { CREATED_ISSUES_EMPTY_STATES } from "constants/dashboard"; import { TIssuesListTypes } from "@plane/types"; // constants -import { CREATED_ISSUES_EMPTY_STATES } from "constants/dashboard"; type Props = { type: TIssuesListTypes; diff --git a/web/components/dashboard/widgets/issue-panels/issue-list-item.tsx b/web/components/dashboard/widgets/issue-panels/issue-list-item.tsx index 716a3afc1..a5279f715 100644 --- a/web/components/dashboard/widgets/issue-panels/issue-list-item.tsx +++ b/web/components/dashboard/widgets/issue-panels/issue-list-item.tsx @@ -1,11 +1,11 @@ -import { observer } from "mobx-react-lite"; import isToday from "date-fns/isToday"; +import { observer } from "mobx-react-lite"; // hooks -import { useIssueDetail, useMember, useProject } from "hooks/store"; // ui import { Avatar, AvatarGroup, ControlLink, PriorityIcon } from "@plane/ui"; // helpers import { findTotalDaysInRange, renderFormattedDate } from "helpers/date-time.helper"; +import { useIssueDetail, useMember, useProject } from "hooks/store"; // types import { TIssue, TWidgetIssue } from "@plane/types"; diff --git a/web/components/dashboard/widgets/issue-panels/issues-list.tsx b/web/components/dashboard/widgets/issue-panels/issues-list.tsx index 16b2b95d9..c429f3599 100644 --- a/web/components/dashboard/widgets/issue-panels/issues-list.tsx +++ b/web/components/dashboard/widgets/issue-panels/issues-list.tsx @@ -1,7 +1,7 @@ import Link from "next/link"; // hooks -import { useIssueDetail } from "hooks/store"; // components +import { Loader, getButtonStyling } from "@plane/ui"; import { AssignedCompletedIssueListItem, AssignedIssuesEmptyState, @@ -14,10 +14,10 @@ import { IssueListItemProps, } from "components/dashboard/widgets"; // ui -import { Loader, getButtonStyling } from "@plane/ui"; // helpers import { cn } from "helpers/common.helper"; import { getRedirectionFilters } from "helpers/dashboard.helper"; +import { useIssueDetail } from "hooks/store"; // types import { TAssignedIssuesWidgetResponse, TCreatedIssuesWidgetResponse, TIssue, TIssuesListTypes } from "@plane/types"; diff --git a/web/components/dashboard/widgets/issue-panels/tabs-list.tsx b/web/components/dashboard/widgets/issue-panels/tabs-list.tsx index d18f08f27..d5fcea697 100644 --- a/web/components/dashboard/widgets/issue-panels/tabs-list.tsx +++ b/web/components/dashboard/widgets/issue-panels/tabs-list.tsx @@ -1,11 +1,11 @@ import { observer } from "mobx-react"; import { Tab } from "@headlessui/react"; // helpers +import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; import { cn } from "helpers/common.helper"; // types import { EDurationFilters, TIssuesListTypes } from "@plane/types"; // constants -import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; type Props = { durationFilter: EDurationFilters; diff --git a/web/components/dashboard/widgets/issues-by-priority.tsx b/web/components/dashboard/widgets/issues-by-priority.tsx index 3e9823fe4..a8a8f64e8 100644 --- a/web/components/dashboard/widgets/issues-by-priority.tsx +++ b/web/components/dashboard/widgets/issues-by-priority.tsx @@ -1,9 +1,8 @@ import { useEffect } from "react"; -import { useRouter } from "next/router"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; +import { useRouter } from "next/router"; // hooks -import { useDashboard } from "hooks/store"; // components import { DurationFilterDropdown, @@ -12,11 +11,10 @@ import { WidgetProps, } from "components/dashboard/widgets"; // helpers -import { getCustomDates } from "helpers/dashboard.helper"; // types -import { EDurationFilters, TIssuesByPriorityWidgetFilters, TIssuesByPriorityWidgetResponse } from "@plane/types"; // constants import { IssuesByPriorityGraph } from "components/graphs"; +import { EDurationFilters, TIssuesByPriorityWidgetFilters, TIssuesByPriorityWidgetResponse } from "@plane/types"; const WIDGET_KEY = "issues_by_priority"; @@ -68,7 +66,7 @@ export const IssuesByPriorityWidget: React.FC = observer((props) => })); return ( -
    +
    = observer((props) => />
    {totalCount > 0 ? ( -
    -
    +
    +
    { @@ -101,7 +99,7 @@ export const IssuesByPriorityWidget: React.FC = observer((props) =>
    ) : ( -
    +
    )} diff --git a/web/components/dashboard/widgets/issues-by-state-group.tsx b/web/components/dashboard/widgets/issues-by-state-group.tsx index b301d30f3..6ffeda0c4 100644 --- a/web/components/dashboard/widgets/issues-by-state-group.tsx +++ b/web/components/dashboard/widgets/issues-by-state-group.tsx @@ -1,19 +1,21 @@ import { useEffect, useState } from "react"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; // hooks -import { useDashboard } from "hooks/store"; -// components -import { PieGraph } from "components/ui"; import { DurationFilterDropdown, IssuesByStateGroupEmptyState, WidgetLoader, WidgetProps, } from "components/dashboard/widgets"; -// helpers +import { PieGraph } from "components/ui"; +import { STATE_GROUP_GRAPH_COLORS, STATE_GROUP_GRAPH_GRADIENTS } from "constants/dashboard"; +import { STATE_GROUPS } from "constants/state"; import { getCustomDates } from "helpers/dashboard.helper"; +import { useDashboard } from "hooks/store"; +// components +// helpers // types import { EDurationFilters, @@ -22,8 +24,6 @@ import { TStateGroups, } from "@plane/types"; // constants -import { STATE_GROUP_GRAPH_COLORS, STATE_GROUP_GRAPH_GRADIENTS } from "constants/dashboard"; -import { STATE_GROUPS } from "constants/state"; const WIDGET_KEY = "issues_by_state_groups"; @@ -84,14 +84,14 @@ export const IssuesByStateGroupWidget: React.FC = observer((props) startedCount > 0 ? "started" : unStartedCount > 0 - ? "unstarted" - : backlogCount > 0 - ? "backlog" - : completedCount > 0 - ? "completed" - : canceledCount > 0 - ? "cancelled" - : null; + ? "unstarted" + : backlogCount > 0 + ? "backlog" + : completedCount > 0 + ? "completed" + : canceledCount > 0 + ? "cancelled" + : null; setActiveStateGroup(stateGroup); setDefaultStateGroup(stateGroup); diff --git a/web/components/dashboard/widgets/loaders/loader.tsx b/web/components/dashboard/widgets/loaders/loader.tsx index 141bb5533..ae4038b38 100644 --- a/web/components/dashboard/widgets/loaders/loader.tsx +++ b/web/components/dashboard/widgets/loaders/loader.tsx @@ -1,13 +1,13 @@ // components +import { TWidgetKeys } from "@plane/types"; import { AssignedIssuesWidgetLoader } from "./assigned-issues"; import { IssuesByPriorityWidgetLoader } from "./issues-by-priority"; import { IssuesByStateGroupWidgetLoader } from "./issues-by-state-group"; import { OverviewStatsWidgetLoader } from "./overview-stats"; import { RecentActivityWidgetLoader } from "./recent-activity"; -import { RecentProjectsWidgetLoader } from "./recent-projects"; import { RecentCollaboratorsWidgetLoader } from "./recent-collaborators"; +import { RecentProjectsWidgetLoader } from "./recent-projects"; // types -import { TWidgetKeys } from "@plane/types"; type Props = { widgetKey: TWidgetKeys; diff --git a/web/components/dashboard/widgets/overview-stats.tsx b/web/components/dashboard/widgets/overview-stats.tsx index 5a105cc15..bfea5bf40 100644 --- a/web/components/dashboard/widgets/overview-stats.tsx +++ b/web/components/dashboard/widgets/overview-stats.tsx @@ -2,14 +2,14 @@ import { useEffect } from "react"; import { observer } from "mobx-react-lite"; import Link from "next/link"; // hooks +import { WidgetLoader } from "components/dashboard/widgets"; +import { cn } from "helpers/common.helper"; +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { useDashboard } from "hooks/store"; // components -import { WidgetLoader } from "components/dashboard/widgets"; // helpers -import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // types import { TOverviewStatsWidgetResponse } from "@plane/types"; -import { cn } from "helpers/common.helper"; export type WidgetProps = { dashboardId: string; @@ -74,6 +74,7 @@ export const OverviewStatsWidget: React.FC = observer((props) => { > {STATS_LIST.map((stat, index) => (
    = observer((props) => { if (!widgetStats) return ; return ( -
    - +
    + Your issue activities {widgetStats.length > 0 ? ( -
    +
    {widgetStats.map((activity) => (
    @@ -49,7 +49,7 @@ export const RecentActivityWidget: React.FC = observer((props) => { activity.new_value === "restore" ? ( ) : ( -
    +
    ) @@ -89,14 +89,14 @@ export const RecentActivityWidget: React.FC = observer((props) => { href={redirectionLink} className={cn( getButtonStyling("link-primary", "sm"), - "w-min mx-auto py-1 px-2 text-xs hover:bg-custom-primary-100/20" + "mx-auto w-min px-2 py-1 text-xs hover:bg-custom-primary-100/20" )} > View all
    ) : ( -
    +
    )} diff --git a/web/components/dashboard/widgets/recent-collaborators.tsx b/web/components/dashboard/widgets/recent-collaborators.tsx new file mode 100644 index 000000000..438f87c45 --- /dev/null +++ b/web/components/dashboard/widgets/recent-collaborators.tsx @@ -0,0 +1,94 @@ +import { useEffect } from "react"; +import { observer } from "mobx-react-lite"; +import Link from "next/link"; +// hooks +import { Avatar } from "@plane/ui"; +import { RecentCollaboratorsEmptyState, WidgetLoader, WidgetProps } from "components/dashboard/widgets"; +import { useDashboard, useMember, useUser } from "hooks/store"; +// components +// ui +// types +import { TRecentCollaboratorsWidgetResponse } from "@plane/types"; + +type CollaboratorListItemProps = { + issueCount: number; + userId: string; + workspaceSlug: string; +}; + +const WIDGET_KEY = "recent_collaborators"; + +const CollaboratorListItem: React.FC = observer((props) => { + const { issueCount, userId, workspaceSlug } = props; + // store hooks + const { currentUser } = useUser(); + const { getUserDetails } = useMember(); + // derived values + const userDetails = getUserDetails(userId); + const isCurrentUser = userId === currentUser?.id; + + if (!userDetails) return null; + + return ( + +
    + +
    +
    + {isCurrentUser ? "You" : userDetails?.display_name} +
    +

    + {issueCount} active issue{issueCount > 1 ? "s" : ""} +

    + + ); +}); + +export const RecentCollaboratorsWidget: React.FC = observer((props) => { + const { dashboardId, workspaceSlug } = props; + // store hooks + const { fetchWidgetStats, getWidgetStats } = useDashboard(); + const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); + + useEffect(() => { + fetchWidgetStats(workspaceSlug, dashboardId, { + widget_key: WIDGET_KEY, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (!widgetStats) return ; + + return ( +
    +
    +

    Most active members

    +

    + Top eight active members in your project by last activity +

    +
    + {widgetStats.length > 1 ? ( +
    + {widgetStats.map((user) => ( + + ))} +
    + ) : ( +
    + +
    + )} +
    + ); +}); diff --git a/web/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx b/web/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx index 48c448075..cfe7dd5ca 100644 --- a/web/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx +++ b/web/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx @@ -1,15 +1,15 @@ import { useEffect } from "react"; -import Link from "next/link"; import { observer } from "mobx-react"; +import Link from "next/link"; import useSWR from "swr"; // store hooks +import { Avatar } from "@plane/ui"; import { useDashboard, useMember, useUser } from "hooks/store"; // components +import { TRecentCollaboratorsWidgetResponse } from "@plane/types"; import { WidgetLoader } from "../loaders"; // ui -import { Avatar } from "@plane/ui"; // types -import { TRecentCollaboratorsWidgetResponse } from "@plane/types"; type CollaboratorListItemProps = { issueCount: number; diff --git a/web/components/dashboard/widgets/recent-collaborators/default-list.tsx b/web/components/dashboard/widgets/recent-collaborators/default-list.tsx index d3f857824..a27534bbf 100644 --- a/web/components/dashboard/widgets/recent-collaborators/default-list.tsx +++ b/web/components/dashboard/widgets/recent-collaborators/default-list.tsx @@ -1,8 +1,8 @@ import { useState } from "react"; // components +import { Button } from "@plane/ui"; import { CollaboratorsList } from "./collaborators-list"; // ui -import { Button } from "@plane/ui"; type Props = { dashboardId: string; diff --git a/web/components/dashboard/widgets/recent-collaborators/root.tsx b/web/components/dashboard/widgets/recent-collaborators/root.tsx index 5f611b462..d65b15db7 100644 --- a/web/components/dashboard/widgets/recent-collaborators/root.tsx +++ b/web/components/dashboard/widgets/recent-collaborators/root.tsx @@ -1,11 +1,10 @@ import { useState } from "react"; import { Search } from "lucide-react"; +// types +import { WidgetProps } from "components/dashboard/widgets"; // components import { DefaultCollaboratorsList } from "./default-list"; import { SearchedCollaboratorsList } from "./search-list"; -8; -// types -import { WidgetProps } from "components/dashboard/widgets"; const PER_PAGE = 8; @@ -15,15 +14,15 @@ export const RecentCollaboratorsWidget: React.FC = (props) => { const [searchQuery, setSearchQuery] = useState(""); return ( -
    -
    +
    +

    Most active members

    Top eight active members in your project by last activity

    -
    +
    = (props) => { const ButtonToRender: React.FC = BORDER_BUTTON_VARIANTS.includes(variant) ? BorderButton : BACKGROUND_BUTTON_VARIANTS.includes(variant) - ? BackgroundButton - : TransparentButton; + ? BackgroundButton + : TransparentButton; return ( { +export const CycleOptions: FC = observer((props) => { const { projectId, isOpen, referenceElement, placement } = props; //state hooks diff --git a/web/components/dropdowns/cycle/index.tsx b/web/components/dropdowns/cycle/index.tsx index 2c05d9ddf..8c08cd67d 100644 --- a/web/components/dropdowns/cycle/index.tsx +++ b/web/components/dropdowns/cycle/index.tsx @@ -3,19 +3,19 @@ import { observer } from "mobx-react-lite"; import { Combobox } from "@headlessui/react"; import { ChevronDown } from "lucide-react"; // hooks +import { ContrastIcon } from "@plane/ui"; +import { cn } from "helpers/common.helper"; import { useCycle } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components import { DropdownButton } from "../buttons"; // icons -import { ContrastIcon } from "@plane/ui"; // helpers -import { cn } from "helpers/common.helper"; // types +import { BUTTON_VARIANTS_WITH_TEXT } from "../constants"; import { TDropdownProps } from "../types"; // constants -import { BUTTON_VARIANTS_WITH_TEXT } from "../constants"; import { CycleOptions } from "./cycle-options"; type Props = TDropdownProps & { diff --git a/web/components/dropdowns/date-range.tsx b/web/components/dropdowns/date-range.tsx index d3ef691b9..421ab41e6 100644 --- a/web/components/dropdowns/date-range.tsx +++ b/web/components/dropdowns/date-range.tsx @@ -1,19 +1,19 @@ import React, { useEffect, useRef, useState } from "react"; -import { Combobox } from "@headlessui/react"; -import { usePopper } from "react-popper"; import { Placement } from "@popperjs/core"; import { DateRange, DayPicker, Matcher } from "react-day-picker"; +import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; import { ArrowRight, CalendarDays } from "lucide-react"; // hooks -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; // components -import { DropdownButton } from "./buttons"; // ui import { Button } from "@plane/ui"; // helpers import { cn } from "helpers/common.helper"; import { renderFormattedDate } from "helpers/date-time.helper"; +import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; +import { DropdownButton } from "./buttons"; // types import { TButtonVariants } from "./types"; diff --git a/web/components/dropdowns/date.tsx b/web/components/dropdowns/date.tsx index 570ea45da..049bf2250 100644 --- a/web/components/dropdowns/date.tsx +++ b/web/components/dropdowns/date.tsx @@ -1,20 +1,20 @@ import React, { useRef, useState } from "react"; -import { Combobox } from "@headlessui/react"; import { DayPicker, Matcher } from "react-day-picker"; import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; import { CalendarDays, X } from "lucide-react"; // hooks +import { cn } from "helpers/common.helper"; +import { renderFormattedDate } from "helpers/date-time.helper"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components import { DropdownButton } from "./buttons"; // helpers -import { renderFormattedDate } from "helpers/date-time.helper"; -import { cn } from "helpers/common.helper"; // types +import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; import { TDropdownProps } from "./types"; // constants -import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; type Props = TDropdownProps & { clearIconClassName?: string; diff --git a/web/components/dropdowns/estimate.tsx b/web/components/dropdowns/estimate.tsx index 663ca67ce..bc977d1ce 100644 --- a/web/components/dropdowns/estimate.tsx +++ b/web/components/dropdowns/estimate.tsx @@ -1,21 +1,21 @@ import { Fragment, ReactNode, useEffect, useRef, useState } from "react"; -import { observer } from "mobx-react-lite"; -import { Combobox } from "@headlessui/react"; -import { usePopper } from "react-popper"; -import { Check, ChevronDown, Search, Triangle } from "lucide-react"; import sortBy from "lodash/sortBy"; +import { observer } from "mobx-react-lite"; +import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; +import { Check, ChevronDown, Search, Triangle } from "lucide-react"; // hooks +import { cn } from "helpers/common.helper"; import { useApplication, useEstimate } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components import { DropdownButton } from "./buttons"; // helpers -import { cn } from "helpers/common.helper"; // types +import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; import { TDropdownProps } from "./types"; // constants -import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; type Props = TDropdownProps & { button?: ReactNode; diff --git a/web/components/dropdowns/member/avatar.tsx b/web/components/dropdowns/member/avatar.tsx index 067d609c5..0f841b9e1 100644 --- a/web/components/dropdowns/member/avatar.tsx +++ b/web/components/dropdowns/member/avatar.tsx @@ -1,8 +1,8 @@ import { observer } from "mobx-react-lite"; // hooks +import { Avatar, AvatarGroup, UserGroupIcon } from "@plane/ui"; import { useMember } from "hooks/store"; // ui -import { Avatar, AvatarGroup, UserGroupIcon } from "@plane/ui"; type AvatarProps = { showTooltip: boolean; diff --git a/web/components/dropdowns/member/index.tsx b/web/components/dropdowns/member/index.tsx index 0513ec627..0e9e36e21 100644 --- a/web/components/dropdowns/member/index.tsx +++ b/web/components/dropdowns/member/index.tsx @@ -3,19 +3,19 @@ import { observer } from "mobx-react-lite"; import { Combobox } from "@headlessui/react"; import { ChevronDown } from "lucide-react"; // hooks +import { cn } from "helpers/common.helper"; import { useMember } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components -import { ButtonAvatars } from "./avatar"; import { DropdownButton } from "../buttons"; +import { BUTTON_VARIANTS_WITH_TEXT } from "../constants"; +import { ButtonAvatars } from "./avatar"; // helpers -import { cn } from "helpers/common.helper"; // types +import { MemberOptions } from "./member-options"; import { MemberDropdownProps } from "./types"; // constants -import { BUTTON_VARIANTS_WITH_TEXT } from "../constants"; -import { MemberOptions } from "./member-options"; type Props = { projectId?: string; diff --git a/web/components/dropdowns/member/member-options.tsx b/web/components/dropdowns/member/member-options.tsx index 46a0b9cba..d91c6e0b1 100644 --- a/web/components/dropdowns/member/member-options.tsx +++ b/web/components/dropdowns/member/member-options.tsx @@ -1,16 +1,16 @@ import { useEffect, useRef, useState } from "react"; -import { Combobox } from "@headlessui/react"; +import { Placement } from "@popperjs/core"; import { observer } from "mobx-react"; +import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; //components +import { Check, Search } from "lucide-react"; import { Avatar } from "@plane/ui"; //store import { useApplication, useMember, useUser } from "hooks/store"; //hooks -import { usePopper } from "react-popper"; //icon -import { Check, Search } from "lucide-react"; //types -import { Placement } from "@popperjs/core"; interface Props { projectId?: string; diff --git a/web/components/dropdowns/module/index.tsx b/web/components/dropdowns/module/index.tsx index 5e0a3977f..882604712 100644 --- a/web/components/dropdowns/module/index.tsx +++ b/web/components/dropdowns/module/index.tsx @@ -3,19 +3,19 @@ import { observer } from "mobx-react-lite"; import { Combobox } from "@headlessui/react"; import { ChevronDown, X } from "lucide-react"; // hooks +import { DiceIcon, Tooltip } from "@plane/ui"; +import { cn } from "helpers/common.helper"; import { useModule } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components import { DropdownButton } from "../buttons"; // icons -import { DiceIcon, Tooltip } from "@plane/ui"; // helpers -import { cn } from "helpers/common.helper"; // types +import { BUTTON_VARIANTS_WITHOUT_TEXT } from "../constants"; import { TDropdownProps } from "../types"; // constants -import { BUTTON_VARIANTS_WITHOUT_TEXT } from "../constants"; import { ModuleOptions } from "./module-options"; type Props = TDropdownProps & { @@ -71,7 +71,7 @@ const ButtonContent: React.FC = (props) => { {showCount ? (
    {!hideIcon && } -
    +
    {value.length > 0 ? value.length === 1 ? `${getModuleById(value[0])?.name || "module"}` @@ -80,18 +80,18 @@ const ButtonContent: React.FC = (props) => {
    ) : value.length > 0 ? ( -
    +
    {value.map((moduleId) => { const moduleDetails = getModuleById(moduleId); return (
    {!hideIcon && } {!hideText && ( - {moduleDetails?.name} + {moduleDetails?.name} )} {!disabled && ( @@ -266,8 +266,7 @@ export const ModuleDropdown: React.FC = observer((props) => { placeholder={placeholder} showCount={showCount} value={value} - // @ts-ignore - onChange={onChange} + onChange={onChange as any} /> diff --git a/web/components/dropdowns/module/module-options.tsx b/web/components/dropdowns/module/module-options.tsx index e7d205b12..8f6a66468 100644 --- a/web/components/dropdowns/module/module-options.tsx +++ b/web/components/dropdowns/module/module-options.tsx @@ -1,17 +1,17 @@ import { useEffect, useRef, useState } from "react"; -import { Combobox } from "@headlessui/react"; +import { Placement } from "@popperjs/core"; import { observer } from "mobx-react"; +import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; //components +import { Check, Search } from "lucide-react"; import { DiceIcon } from "@plane/ui"; //store +import { cn } from "helpers/common.helper"; import { useApplication, useModule } from "hooks/store"; //hooks -import { usePopper } from "react-popper"; -import { cn } from "helpers/common.helper"; //icon -import { Check, Search } from "lucide-react"; //types -import { Placement } from "@popperjs/core"; type DropdownOptions = | { diff --git a/web/components/dropdowns/priority.tsx b/web/components/dropdowns/priority.tsx index e0677c843..2409971f3 100644 --- a/web/components/dropdowns/priority.tsx +++ b/web/components/dropdowns/priority.tsx @@ -1,21 +1,21 @@ import { Fragment, ReactNode, useEffect, useRef, useState } from "react"; -import { Combobox } from "@headlessui/react"; -import { usePopper } from "react-popper"; -import { Check, ChevronDown, Search } from "lucide-react"; import { useTheme } from "next-themes"; +import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; +import { Check, ChevronDown, Search } from "lucide-react"; // hooks +import { PriorityIcon, Tooltip } from "@plane/ui"; +import { ISSUE_PRIORITIES } from "constants/issue"; +import { cn } from "helpers/common.helper"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // icons -import { PriorityIcon, Tooltip } from "@plane/ui"; // helpers -import { cn } from "helpers/common.helper"; // types import { TIssuePriorities } from "@plane/types"; +import { BACKGROUND_BUTTON_VARIANTS, BORDER_BUTTON_VARIANTS, BUTTON_VARIANTS_WITHOUT_TEXT } from "./constants"; import { TDropdownProps } from "./types"; // constants -import { ISSUE_PRIORITIES } from "constants/issue"; -import { BACKGROUND_BUTTON_VARIANTS, BORDER_BUTTON_VARIANTS, BUTTON_VARIANTS_WITHOUT_TEXT } from "./constants"; type Props = TDropdownProps & { button?: ReactNode; @@ -342,8 +342,8 @@ export const PriorityDropdown: React.FC = (props) => { const ButtonToRender = BORDER_BUTTON_VARIANTS.includes(buttonVariant) ? BorderButton : BACKGROUND_BUTTON_VARIANTS.includes(buttonVariant) - ? BackgroundButton - : TransparentButton; + ? BackgroundButton + : TransparentButton; useEffect(() => { if (isOpen && inputRef.current) { diff --git a/web/components/dropdowns/project.tsx b/web/components/dropdowns/project.tsx index f6fb9205e..05b455e5e 100644 --- a/web/components/dropdowns/project.tsx +++ b/web/components/dropdowns/project.tsx @@ -1,21 +1,21 @@ import { Fragment, ReactNode, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react-lite"; -import { Combobox } from "@headlessui/react"; import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; import { Check, ChevronDown, Search } from "lucide-react"; // hooks +import { cn } from "helpers/common.helper"; +import { renderEmoji } from "helpers/emoji.helper"; import { useProject } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components import { DropdownButton } from "./buttons"; // helpers -import { cn } from "helpers/common.helper"; -import { renderEmoji } from "helpers/emoji.helper"; // types +import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; import { TDropdownProps } from "./types"; // constants -import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; type Props = TDropdownProps & { button?: ReactNode; @@ -81,8 +81,8 @@ export const ProjectDropdown: React.FC = observer((props) => { {projectDetails?.emoji ? renderEmoji(projectDetails?.emoji) : projectDetails?.icon_prop - ? renderEmoji(projectDetails?.icon_prop) - : null} + ? renderEmoji(projectDetails?.icon_prop) + : null} {projectDetails?.name}
    @@ -174,8 +174,8 @@ export const ProjectDropdown: React.FC = observer((props) => { {selectedProject?.emoji ? renderEmoji(selectedProject?.emoji) : selectedProject?.icon_prop - ? renderEmoji(selectedProject?.icon_prop) - : null} + ? renderEmoji(selectedProject?.icon_prop) + : null} )} {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && ( diff --git a/web/components/dropdowns/state.tsx b/web/components/dropdowns/state.tsx index 9fa2f38c8..f34ef576c 100644 --- a/web/components/dropdowns/state.tsx +++ b/web/components/dropdowns/state.tsx @@ -1,22 +1,22 @@ import { Fragment, ReactNode, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react-lite"; -import { Combobox } from "@headlessui/react"; import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; import { Check, ChevronDown, Search } from "lucide-react"; // hooks +import { StateGroupIcon } from "@plane/ui"; +import { cn } from "helpers/common.helper"; import { useApplication, useProjectState } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components import { DropdownButton } from "./buttons"; // icons -import { StateGroupIcon } from "@plane/ui"; // helpers -import { cn } from "helpers/common.helper"; // types +import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; import { TDropdownProps } from "./types"; // constants -import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; type Props = TDropdownProps & { button?: ReactNode; diff --git a/web/components/emoji-icon-picker/index.tsx b/web/components/emoji-icon-picker/index.tsx index 9c45e5356..b9211a718 100644 --- a/web/components/emoji-icon-picker/index.tsx +++ b/web/components/emoji-icon-picker/index.tsx @@ -1,19 +1,19 @@ import React, { useEffect, useState, useRef } from "react"; // headless ui +import { TwitterPicker } from "react-color"; import { Tab, Transition, Popover } from "@headlessui/react"; // react colors -import { TwitterPicker } from "react-color"; // hooks +import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper"; import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // types -import { Props } from "./types"; // emojis import emojis from "./emojis.json"; +import { getRecentEmojis, saveRecentEmoji } from "./helpers"; import icons from "./icons.json"; // helpers -import { getRecentEmojis, saveRecentEmoji } from "./helpers"; -import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper"; +import { Props } from "./types"; const tabOptions = [ { diff --git a/web/components/empty-state/comic-box-button.tsx b/web/components/empty-state/comic-box-button.tsx index 607d74a91..0bf546a2f 100644 --- a/web/components/empty-state/comic-box-button.tsx +++ b/web/components/empty-state/comic-box-button.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; +import { usePopper } from "react-popper"; import { Popover } from "@headlessui/react"; // popper -import { usePopper } from "react-popper"; // helper import { getButtonStyling } from "@plane/ui"; diff --git a/web/components/empty-state/empty-state.tsx b/web/components/empty-state/empty-state.tsx index 4a5aeca02..9d77a81d0 100644 --- a/web/components/empty-state/empty-state.tsx +++ b/web/components/empty-state/empty-state.tsx @@ -1,11 +1,11 @@ import React from "react"; import Image from "next/image"; // components -import { ComicBoxButton } from "./comic-box-button"; // ui import { Button, getButtonStyling } from "@plane/ui"; // helper import { cn } from "helpers/common.helper"; +import { ComicBoxButton } from "./comic-box-button"; type Props = { title: string; diff --git a/web/components/estimates/create-update-estimate-modal.tsx b/web/components/estimates/create-update-estimate-modal.tsx index 1ca39c84a..3be83e319 100644 --- a/web/components/estimates/create-update-estimate-modal.tsx +++ b/web/components/estimates/create-update-estimate-modal.tsx @@ -1,14 +1,14 @@ import React, { useEffect } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; -import { observer } from "mobx-react-lite"; // store hooks +import { Button, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; +import { checkDuplicates } from "helpers/array.helper"; import { useEstimate } from "hooks/store"; // ui -import { Button, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; // helpers -import { checkDuplicates } from "helpers/array.helper"; // types import { IEstimate, IEstimateFormData } from "@plane/types"; @@ -269,7 +269,7 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { {Array(6) .fill(0) .map((_, i) => ( -
    +
    {i + 1} diff --git a/web/components/estimates/delete-estimate-modal.tsx b/web/components/estimates/delete-estimate-modal.tsx index ac51d2312..f8bc2a65b 100644 --- a/web/components/estimates/delete-estimate-modal.tsx +++ b/web/components/estimates/delete-estimate-modal.tsx @@ -1,14 +1,14 @@ import React, { useEffect, useState } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; -import { observer } from "mobx-react-lite"; import { AlertTriangle } from "lucide-react"; // store hooks +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; import { useEstimate } from "hooks/store"; // types import { IEstimate } from "@plane/types"; // ui -import { Button, TOAST_TYPE, setToast } from "@plane/ui"; type Props = { isOpen: boolean; @@ -29,6 +29,7 @@ export const DeleteEstimateModal: React.FC = observer((props) => { const handleEstimateDelete = () => { if (!workspaceSlug || !projectId) return; + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain const estimateId = data?.id!; deleteEstimate(workspaceSlug.toString(), projectId.toString(), estimateId) diff --git a/web/components/estimates/estimate-list-item.tsx b/web/components/estimates/estimate-list-item.tsx index 37932a0ac..c63c4b208 100644 --- a/web/components/estimates/estimate-list-item.tsx +++ b/web/components/estimates/estimate-list-item.tsx @@ -1,14 +1,14 @@ import React from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { Pencil, Trash2 } from "lucide-react"; +import { Button, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; +import { orderArrayBy } from "helpers/array.helper"; import { useProject } from "hooks/store"; // ui -import { Button, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; //icons -import { Pencil, Trash2 } from "lucide-react"; // helpers -import { orderArrayBy } from "helpers/array.helper"; // types import { IEstimate } from "@plane/types"; diff --git a/web/components/estimates/estimates-list.tsx b/web/components/estimates/estimates-list.tsx index 711f713a6..8e447d6ac 100644 --- a/web/components/estimates/estimates-list.tsx +++ b/web/components/estimates/estimates-list.tsx @@ -1,20 +1,20 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { useTheme } from "next-themes"; // store hooks +import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "components/estimates"; +import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { orderArrayBy } from "helpers/array.helper"; import { useEstimate, useProject, useUser } from "hooks/store"; // components -import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "components/estimates"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui -import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IEstimate } from "@plane/types"; // helpers -import { orderArrayBy } from "helpers/array.helper"; // constants -import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; export const EstimatesList: React.FC = observer(() => { // states diff --git a/web/components/exporter/export-modal.tsx b/web/components/exporter/export-modal.tsx index f38550b3a..16f8d4640 100644 --- a/web/components/exporter/export-modal.tsx +++ b/web/components/exporter/export-modal.tsx @@ -1,13 +1,14 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; // hooks +import { Button, CustomSearchSelect, TOAST_TYPE, setToast } from "@plane/ui"; + import { useProject } from "hooks/store"; // services import { ProjectExportService } from "services/project"; // ui -import { Button, CustomSearchSelect, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IUser, IImporterService } from "@plane/types"; diff --git a/web/components/exporter/guide.tsx b/web/components/exporter/guide.tsx index ed6a39220..381b168bd 100644 --- a/web/components/exporter/guide.tsx +++ b/web/components/exporter/guide.tsx @@ -1,30 +1,29 @@ import { useState } from "react"; -import Link from "next/link"; +import { observer } from "mobx-react-lite"; import Image from "next/image"; +import Link from "next/link"; import { useRouter } from "next/router"; import { useTheme } from "next-themes"; import useSWR, { mutate } from "swr"; -import { observer } from "mobx-react-lite"; // hooks +import { MoveLeft, MoveRight, RefreshCw } from "lucide-react"; +import { Button } from "@plane/ui"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { Exporter, SingleExport } from "components/exporter"; +import { ImportExportSettingsLoader } from "components/ui"; +import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EXPORT_SERVICES_LIST } from "constants/fetch-keys"; +import { EXPORTERS_LIST } from "constants/workspace"; import { useUser } from "hooks/store"; import useUserAuth from "hooks/use-user-auth"; // services import { IntegrationService } from "services/integrations"; // components -import { Exporter, SingleExport } from "components/exporter"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui -import { Button } from "@plane/ui"; -import { ImportExportSettingsLoader } from "components/ui"; // icons -import { MoveLeft, MoveRight, RefreshCw } from "lucide-react"; // fetch-keys -import { EXPORT_SERVICES_LIST } from "constants/fetch-keys"; // constants -import { EXPORTERS_LIST } from "constants/workspace"; - -import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; // services const integrationService = new IntegrationService(); diff --git a/web/components/exporter/single-export.tsx b/web/components/exporter/single-export.tsx index 34e41fc35..4fdcb4a15 100644 --- a/web/components/exporter/single-export.tsx +++ b/web/components/exporter/single-export.tsx @@ -38,12 +38,12 @@ export const SingleExport: FC = ({ service, refreshing }) => { service.status === "completed" ? "bg-green-500/20 text-green-500" : service.status === "processing" - ? "bg-yellow-500/20 text-yellow-500" - : service.status === "failed" - ? "bg-red-500/20 text-red-500" - : service.status === "expired" - ? "bg-orange-500/20 text-orange-500" - : "" + ? "bg-yellow-500/20 text-yellow-500" + : service.status === "failed" + ? "bg-red-500/20 text-red-500" + : service.status === "expired" + ? "bg-orange-500/20 text-orange-500" + : "" }`} > {refreshing ? "Refreshing..." : service.status} diff --git a/web/components/gantt-chart/blocks/block.tsx b/web/components/gantt-chart/blocks/block.tsx index 1e0882aee..3305c9846 100644 --- a/web/components/gantt-chart/blocks/block.tsx +++ b/web/components/gantt-chart/blocks/block.tsx @@ -1,16 +1,16 @@ import { observer } from "mobx-react"; // hooks -import { useGanttChart } from "../hooks"; -import { useIssueDetail } from "hooks/store"; // components -import { ChartAddBlock, ChartDraggable } from "../helpers"; // helpers import { cn } from "helpers/common.helper"; import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { useIssueDetail } from "hooks/store"; // types -import { IBlockUpdateData, IGanttBlock } from "../types"; // constants import { BLOCK_HEIGHT } from "../constants"; +import { ChartAddBlock, ChartDraggable } from "../helpers"; +import { useGanttChart } from "../hooks"; +import { IBlockUpdateData, IGanttBlock } from "../types"; type Props = { block: IGanttBlock; diff --git a/web/components/gantt-chart/blocks/blocks-list.tsx b/web/components/gantt-chart/blocks/blocks-list.tsx index d98524ecc..8eb1d8772 100644 --- a/web/components/gantt-chart/blocks/blocks-list.tsx +++ b/web/components/gantt-chart/blocks/blocks-list.tsx @@ -1,10 +1,10 @@ import { FC } from "react"; // components +import { HEADER_HEIGHT } from "../constants"; +import { IBlockUpdateData, IGanttBlock } from "../types"; import { GanttChartBlock } from "./block"; // types -import { IBlockUpdateData, IGanttBlock } from "../types"; // constants -import { HEADER_HEIGHT } from "../constants"; export type GanttChartBlocksProps = { itemsContainerWidth: number; @@ -47,6 +47,7 @@ export const GanttChartBlocksList: FC = (props) => { return ( = observer(() => { // chart hook diff --git a/web/components/gantt-chart/contexts/index.tsx b/web/components/gantt-chart/contexts/index.tsx index 1d8a19f1a..752645f66 100644 --- a/web/components/gantt-chart/contexts/index.tsx +++ b/web/components/gantt-chart/contexts/index.tsx @@ -1,4 +1,4 @@ -import { createContext } from "react"; +import React, { FC, createContext } from "react"; // mobx store import { GanttStore } from "store/issue/issue_gantt_view.store"; @@ -7,13 +7,17 @@ let ganttViewStore = new GanttStore(); export const GanttStoreContext = createContext(ganttViewStore); const initializeStore = () => { - const _ganttStore = ganttViewStore ?? new GanttStore(); - if (typeof window === "undefined") return _ganttStore; - if (!ganttViewStore) ganttViewStore = _ganttStore; - return _ganttStore; + const newGanttViewStore = ganttViewStore ?? new GanttStore(); + if (typeof window === "undefined") return newGanttViewStore; + if (!ganttViewStore) ganttViewStore = newGanttViewStore; + return newGanttViewStore; }; -export const GanttStoreProvider = ({ children }: any) => { +type GanttStoreProviderProps = { + children: React.ReactNode; +}; + +export const GanttStoreProvider: FC = ({ children }) => { const store = initializeStore(); return {children}; }; diff --git a/web/components/gantt-chart/helpers/add-block.tsx b/web/components/gantt-chart/helpers/add-block.tsx index b7497013f..d12c9f20e 100644 --- a/web/components/gantt-chart/helpers/add-block.tsx +++ b/web/components/gantt-chart/helpers/add-block.tsx @@ -1,14 +1,14 @@ import { useEffect, useRef, useState } from "react"; import { addDays } from "date-fns"; +import { observer } from "mobx-react"; import { Plus } from "lucide-react"; // ui import { Tooltip } from "@plane/ui"; // helpers import { renderFormattedDate, renderFormattedPayloadDate } from "helpers/date-time.helper"; // types -import { IBlockUpdateData, IGanttBlock } from "../types"; import { useGanttChart } from "../hooks/use-gantt-chart"; -import { observer } from "mobx-react"; +import { IBlockUpdateData, IGanttBlock } from "../types"; type Props = { block: IGanttBlock; diff --git a/web/components/gantt-chart/helpers/draggable.tsx b/web/components/gantt-chart/helpers/draggable.tsx index c2b4dc619..54590c372 100644 --- a/web/components/gantt-chart/helpers/draggable.tsx +++ b/web/components/gantt-chart/helpers/draggable.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useRef, useState } from "react"; +import { observer } from "mobx-react"; import { ArrowRight } from "lucide-react"; // hooks import { IGanttBlock } from "components/gantt-chart"; @@ -7,7 +8,6 @@ import { cn } from "helpers/common.helper"; // constants import { SIDEBAR_WIDTH } from "../constants"; import { useGanttChart } from "../hooks/use-gantt-chart"; -import { observer } from "mobx-react"; type Props = { block: IGanttBlock; diff --git a/web/components/gantt-chart/sidebar/cycles/block.tsx b/web/components/gantt-chart/sidebar/cycles/block.tsx index f1374c753..6e780c479 100644 --- a/web/components/gantt-chart/sidebar/cycles/block.tsx +++ b/web/components/gantt-chart/sidebar/cycles/block.tsx @@ -2,16 +2,16 @@ import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; import { observer } from "mobx-react"; import { MoreVertical } from "lucide-react"; // hooks +import { CycleGanttSidebarBlock } from "components/cycles"; +import { BLOCK_HEIGHT } from "components/gantt-chart/constants"; import { useGanttChart } from "components/gantt-chart/hooks"; // components -import { CycleGanttSidebarBlock } from "components/cycles"; // helpers +import { IGanttBlock } from "components/gantt-chart/types"; import { cn } from "helpers/common.helper"; import { findTotalDaysInRange } from "helpers/date-time.helper"; // types -import { IGanttBlock } from "components/gantt-chart/types"; // constants -import { BLOCK_HEIGHT } from "components/gantt-chart/constants"; type Props = { block: IGanttBlock; diff --git a/web/components/gantt-chart/sidebar/cycles/sidebar.tsx b/web/components/gantt-chart/sidebar/cycles/sidebar.tsx index 11f67a099..e47b2304e 100644 --- a/web/components/gantt-chart/sidebar/cycles/sidebar.tsx +++ b/web/components/gantt-chart/sidebar/cycles/sidebar.tsx @@ -2,9 +2,9 @@ import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea // ui import { Loader } from "@plane/ui"; // components +import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types"; import { CyclesSidebarBlock } from "./block"; // types -import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types"; type Props = { title: string; diff --git a/web/components/gantt-chart/sidebar/issues/block.tsx b/web/components/gantt-chart/sidebar/issues/block.tsx index 03a17a65b..92fc32664 100644 --- a/web/components/gantt-chart/sidebar/issues/block.tsx +++ b/web/components/gantt-chart/sidebar/issues/block.tsx @@ -2,17 +2,17 @@ import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; import { observer } from "mobx-react"; import { MoreVertical } from "lucide-react"; // hooks -import { useIssueDetail } from "hooks/store"; import { useGanttChart } from "components/gantt-chart/hooks"; // components import { IssueGanttSidebarBlock } from "components/issues"; // helpers import { cn } from "helpers/common.helper"; import { findTotalDaysInRange } from "helpers/date-time.helper"; +import { useIssueDetail } from "hooks/store"; // types -import { IGanttBlock } from "../../types"; // constants import { BLOCK_HEIGHT } from "../../constants"; +import { IGanttBlock } from "../../types"; type Props = { block: IGanttBlock; diff --git a/web/components/gantt-chart/sidebar/issues/sidebar.tsx b/web/components/gantt-chart/sidebar/issues/sidebar.tsx index 323938eec..e82e40f5d 100644 --- a/web/components/gantt-chart/sidebar/issues/sidebar.tsx +++ b/web/components/gantt-chart/sidebar/issues/sidebar.tsx @@ -1,10 +1,10 @@ import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd"; // components -import { IssuesSidebarBlock } from "./block"; // ui import { Loader } from "@plane/ui"; // types import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types"; +import { IssuesSidebarBlock } from "./block"; type Props = { blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; diff --git a/web/components/gantt-chart/sidebar/modules/block.tsx b/web/components/gantt-chart/sidebar/modules/block.tsx index 4b2e47226..41647644f 100644 --- a/web/components/gantt-chart/sidebar/modules/block.tsx +++ b/web/components/gantt-chart/sidebar/modules/block.tsx @@ -2,16 +2,16 @@ import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; import { observer } from "mobx-react"; import { MoreVertical } from "lucide-react"; // hooks +import { BLOCK_HEIGHT } from "components/gantt-chart/constants"; import { useGanttChart } from "components/gantt-chart/hooks"; // components +import { IGanttBlock } from "components/gantt-chart/types"; import { ModuleGanttSidebarBlock } from "components/modules"; // helpers import { cn } from "helpers/common.helper"; import { findTotalDaysInRange } from "helpers/date-time.helper"; // types -import { IGanttBlock } from "components/gantt-chart/types"; // constants -import { BLOCK_HEIGHT } from "components/gantt-chart/constants"; type Props = { block: IGanttBlock; diff --git a/web/components/gantt-chart/sidebar/modules/sidebar.tsx b/web/components/gantt-chart/sidebar/modules/sidebar.tsx index dee83fa79..a4bcbd5ec 100644 --- a/web/components/gantt-chart/sidebar/modules/sidebar.tsx +++ b/web/components/gantt-chart/sidebar/modules/sidebar.tsx @@ -2,9 +2,9 @@ import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea // ui import { Loader } from "@plane/ui"; // components +import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart"; import { ModulesSidebarBlock } from "./block"; // types -import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart"; type Props = { title: string; diff --git a/web/components/gantt-chart/sidebar/project-views.tsx b/web/components/gantt-chart/sidebar/project-views.tsx index a7e7c5e35..92a677b19 100644 --- a/web/components/gantt-chart/sidebar/project-views.tsx +++ b/web/components/gantt-chart/sidebar/project-views.tsx @@ -2,9 +2,9 @@ import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea // ui import { Loader } from "@plane/ui"; // components +import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types"; import { IssuesSidebarBlock } from "./issues/block"; // types -import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types"; type Props = { title: string; diff --git a/web/components/gantt-chart/views/bi-week-view.ts b/web/components/gantt-chart/views/bi-week-view.ts index 14c0aad15..6ace4bcc4 100644 --- a/web/components/gantt-chart/views/bi-week-view.ts +++ b/web/components/gantt-chart/views/bi-week-view.ts @@ -1,7 +1,7 @@ // types +import { weeks, months } from "../data"; import { ChartDataType } from "../types"; // data -import { weeks, months } from "../data"; // helpers import { generateDate, getWeekNumberByDate, getNumberOfDaysInMonth, getDatesBetweenTwoDates } from "./helpers"; diff --git a/web/components/gantt-chart/views/day-view.ts b/web/components/gantt-chart/views/day-view.ts index 0801b7bb1..e8da6801c 100644 --- a/web/components/gantt-chart/views/day-view.ts +++ b/web/components/gantt-chart/views/day-view.ts @@ -1,7 +1,7 @@ // types +import { weeks, months } from "../data"; import { ChartDataType } from "../types"; // data -import { weeks, months } from "../data"; export const getWeekNumberByDate = (date: Date) => { const firstDayOfYear = new Date(date.getFullYear(), 0, 1); diff --git a/web/components/gantt-chart/views/helpers.ts b/web/components/gantt-chart/views/helpers.ts index 94b614286..4bd295ce3 100644 --- a/web/components/gantt-chart/views/helpers.ts +++ b/web/components/gantt-chart/views/helpers.ts @@ -56,8 +56,8 @@ export const getAllDatesInWeekByWeekNumber = (weekNumber: number, year: number) const startDate = new Date(firstDayOfYear.getTime()); startDate.setDate(startDate.getDate() + 7 * (weekNumber - 1)); - var datesInWeek = []; - for (var i = 0; i < 7; i++) { + const datesInWeek = []; + for (let i = 0; i < 7; i++) { const currentDate = new Date(startDate.getTime()); currentDate.setDate(currentDate.getDate() + i); datesInWeek.push(currentDate); diff --git a/web/components/gantt-chart/views/hours-view.ts b/web/components/gantt-chart/views/hours-view.ts index 0801b7bb1..e8da6801c 100644 --- a/web/components/gantt-chart/views/hours-view.ts +++ b/web/components/gantt-chart/views/hours-view.ts @@ -1,7 +1,7 @@ // types +import { weeks, months } from "../data"; import { ChartDataType } from "../types"; // data -import { weeks, months } from "../data"; export const getWeekNumberByDate = (date: Date) => { const firstDayOfYear = new Date(date.getFullYear(), 0, 1); diff --git a/web/components/gantt-chart/views/month-view.ts b/web/components/gantt-chart/views/month-view.ts index 13d054da1..1e7e6d878 100644 --- a/web/components/gantt-chart/views/month-view.ts +++ b/web/components/gantt-chart/views/month-view.ts @@ -1,7 +1,7 @@ // types +import { weeks, months } from "../data"; import { ChartDataType, IGanttBlock } from "../types"; // data -import { weeks, months } from "../data"; // helpers import { generateDate, getWeekNumberByDate, getNumberOfDaysInMonth, getDatesBetweenTwoDates } from "./helpers"; @@ -178,7 +178,7 @@ export const getMonthChartItemPositionWidthInMonth = (chartData: ChartDataType, const positionDaysDifference: number = Math.abs(Math.floor(positionTimeDifference / (1000 * 60 * 60 * 24))); scrollPosition = positionDaysDifference * chartData.data.width; - var diffMonths = (itemStartDate.getFullYear() - startDate.getFullYear()) * 12; + let diffMonths = (itemStartDate.getFullYear() - startDate.getFullYear()) * 12; diffMonths -= startDate.getMonth(); diffMonths += itemStartDate.getMonth(); diff --git a/web/components/gantt-chart/views/quater-view.ts b/web/components/gantt-chart/views/quater-view.ts index ed25974a3..9d45a43a1 100644 --- a/web/components/gantt-chart/views/quater-view.ts +++ b/web/components/gantt-chart/views/quater-view.ts @@ -1,7 +1,7 @@ // types +import { weeks, months } from "../data"; import { ChartDataType } from "../types"; // data -import { weeks, months } from "../data"; // helpers import { getDatesBetweenTwoDates, getWeeksByMonthAndYear } from "./helpers"; diff --git a/web/components/gantt-chart/views/week-view.ts b/web/components/gantt-chart/views/week-view.ts index a65eb70b9..bd4ae383d 100644 --- a/web/components/gantt-chart/views/week-view.ts +++ b/web/components/gantt-chart/views/week-view.ts @@ -1,7 +1,7 @@ // types +import { weeks, months } from "../data"; import { ChartDataType } from "../types"; // data -import { weeks, months } from "../data"; // helpers import { generateDate, getWeekNumberByDate, getNumberOfDaysInMonth, getDatesBetweenTwoDates } from "./helpers"; diff --git a/web/components/gantt-chart/views/year-view.ts b/web/components/gantt-chart/views/year-view.ts index 82d397e97..69ff9dae8 100644 --- a/web/components/gantt-chart/views/year-view.ts +++ b/web/components/gantt-chart/views/year-view.ts @@ -1,7 +1,7 @@ // types +import { weeks, months } from "../data"; import { ChartDataType } from "../types"; // data -import { weeks, months } from "../data"; // helpers import { getDatesBetweenTwoDates, getWeeksByMonthAndYear } from "./helpers"; diff --git a/web/components/graphs/issues-by-priority.tsx b/web/components/graphs/issues-by-priority.tsx index 0d4bf37b5..9dfe56891 100644 --- a/web/components/graphs/issues-by-priority.tsx +++ b/web/components/graphs/issues-by-priority.tsx @@ -1,14 +1,14 @@ -import { Theme } from "@nivo/core"; import { ComputedDatum } from "@nivo/bar"; +import { Theme } from "@nivo/core"; // components import { BarGraph } from "components/ui"; // helpers +import { PRIORITY_GRAPH_GRADIENTS } from "constants/dashboard"; +import { ISSUE_PRIORITIES } from "constants/issue"; import { capitalizeFirstLetter } from "helpers/string.helper"; // types import { TIssuePriorities } from "@plane/types"; // constants -import { PRIORITY_GRAPH_GRADIENTS } from "constants/dashboard"; -import { ISSUE_PRIORITIES } from "constants/issue"; type Props = { borderRadius?: number; diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index 7cdc23133..18d0543c0 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -1,8 +1,20 @@ import { useCallback, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import Link from "next/link"; +import { useRouter } from "next/router"; // hooks +import { ArrowRight, Plus, PanelRight } from "lucide-react"; +import { Breadcrumbs, Button, ContrastIcon, CustomMenu } from "@plane/ui"; +import { ProjectAnalyticsModal } from "components/analytics"; +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { CycleMobileHeader } from "components/cycles/cycle-mobile-header"; +import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; +import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { cn } from "helpers/common.helper"; +import { renderEmoji } from "helpers/emoji.helper"; +import { truncateText } from "helpers/string.helper"; import { useApplication, useEventTracker, @@ -16,24 +28,12 @@ import { } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // components -import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; -import { ProjectAnalyticsModal } from "components/analytics"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; // ui -import { Breadcrumbs, Button, ContrastIcon, CustomMenu } from "@plane/ui"; // icons -import { ArrowRight, Plus, PanelRight } from "lucide-react"; // helpers -import { truncateText } from "helpers/string.helper"; -import { renderEmoji } from "helpers/emoji.helper"; // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; // constants -import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { cn } from "helpers/common.helper"; -import { CycleMobileHeader } from "components/cycles/cycle-mobile-header"; const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => { // router @@ -209,9 +209,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { className="ml-1.5 flex-shrink-0" placement="bottom-start" > - {currentProjectCycleIds?.map((cycleId) => ( - - ))} + {currentProjectCycleIds?.map((cycleId) => )} } /> diff --git a/web/components/headers/cycles.tsx b/web/components/headers/cycles.tsx index 496fabecd..a0ab19ec7 100644 --- a/web/components/headers/cycles.tsx +++ b/web/components/headers/cycles.tsx @@ -1,20 +1,20 @@ import { FC, useCallback } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { List, Plus } from "lucide-react"; // hooks -import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; // ui import { Breadcrumbs, Button, ContrastIcon, CustomMenu } from "@plane/ui"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; -import { EUserProjectRoles } from "constants/project"; // components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { BreadcrumbLink } from "components/common"; -import { TCycleLayout } from "@plane/types"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { CYCLE_VIEW_LAYOUTS } from "constants/cycle"; +import { EUserProjectRoles } from "constants/project"; +import { renderEmoji } from "helpers/emoji.helper"; +import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; +import { TCycleLayout } from "@plane/types"; export const CyclesHeader: FC = observer(() => { // router @@ -73,7 +73,9 @@ export const CyclesHeader: FC = observer(() => { /> } />} + link={ + } /> + } />
    @@ -110,6 +112,7 @@ export const CyclesHeader: FC = observer(() => { > {CYCLE_VIEW_LAYOUTS.map((layout) => ( { // handleLayoutChange(ISSUE_LAYOUTS[index].key); handleCurrentLayout(layout.key as TCycleLayout); diff --git a/web/components/headers/global-issues.tsx b/web/components/headers/global-issues.tsx index 3c40cbbff..effe60fe4 100644 --- a/web/components/headers/global-issues.tsx +++ b/web/components/headers/global-issues.tsx @@ -1,23 +1,23 @@ import { useCallback, useState } from "react"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; // hooks -import { useLabel, useMember, useUser, useIssues } from "hooks/store"; -// components -import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "components/issues"; -import { CreateUpdateWorkspaceViewModal } from "components/workspace"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; -// ui -import { Breadcrumbs, Button, LayersIcon, PhotoFilterIcon, Tooltip } from "@plane/ui"; -// icons import { List, PlusIcon, Sheet } from "lucide-react"; +import { Breadcrumbs, Button, LayersIcon, PhotoFilterIcon, Tooltip } from "@plane/ui"; +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "components/issues"; +// components +import { CreateUpdateWorkspaceViewModal } from "components/workspace"; +// ui +// icons // types -import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; // constants import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { EUserWorkspaceRoles } from "constants/workspace"; +import { useLabel, useMember, useUser, useIssues } from "hooks/store"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; const GLOBAL_VIEW_LAYOUTS = [ { key: "list", title: "List", link: "/workspace-views", icon: List }, diff --git a/web/components/headers/module-issues.tsx b/web/components/headers/module-issues.tsx index b84504ee2..ca3a84e3b 100644 --- a/web/components/headers/module-issues.tsx +++ b/web/components/headers/module-issues.tsx @@ -1,8 +1,20 @@ import { useCallback, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import Link from "next/link"; +import { useRouter } from "next/router"; // hooks +import { ArrowRight, PanelRight, Plus } from "lucide-react"; +import { Breadcrumbs, Button, CustomMenu, DiceIcon } from "@plane/ui"; +import { ProjectAnalyticsModal } from "components/analytics"; +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; +import { ModuleMobileHeader } from "components/modules/module-mobile-header"; +import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { cn } from "helpers/common.helper"; +import { renderEmoji } from "helpers/emoji.helper"; +import { truncateText } from "helpers/string.helper"; import { useApplication, useEventTracker, @@ -16,24 +28,12 @@ import { } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // components -import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; -import { ProjectAnalyticsModal } from "components/analytics"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; // ui -import { Breadcrumbs, Button, CustomMenu, DiceIcon, LayersIcon } from "@plane/ui"; // icons -import { ArrowRight, PanelRight, Plus } from "lucide-react"; // helpers -import { truncateText } from "helpers/string.helper"; -import { renderEmoji } from "helpers/emoji.helper"; // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; // constants -import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { cn } from "helpers/common.helper"; -import { ModuleMobileHeader } from "components/modules/module-mobile-header"; const ModuleDropdownOption: React.FC<{ moduleId: string }> = ({ moduleId }) => { // router @@ -212,9 +212,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => { className="ml-1.5 flex-shrink-0" placement="bottom-start" > - {projectModuleIds?.map((moduleId) => ( - - ))} + {projectModuleIds?.map((moduleId) => )} } /> diff --git a/web/components/headers/modules-list.tsx b/web/components/headers/modules-list.tsx index 9ad34678a..b942b7b13 100644 --- a/web/components/headers/modules-list.tsx +++ b/web/components/headers/modules-list.tsx @@ -1,19 +1,20 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; +// icons import { GanttChartSquare, LayoutGrid, List, Plus } from "lucide-react"; -// hooks -import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; -import useLocalStorage from "hooks/use-local-storage"; // ui import { Breadcrumbs, Button, Tooltip, DiceIcon, CustomMenu } from "@plane/ui"; -// helper -import { renderEmoji } from "helpers/emoji.helper"; +// components +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; // constants import { MODULE_VIEW_LAYOUTS } from "constants/module"; import { EUserProjectRoles } from "constants/project"; -// components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; +// helper +import { renderEmoji } from "helpers/emoji.helper"; +// hooks +import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; +import useLocalStorage from "hooks/use-local-storage"; export const ModulesListHeader: React.FC = observer(() => { // router @@ -71,14 +72,16 @@ export const ModulesListHeader: React.FC = observer(() => { @@ -106,7 +109,13 @@ export const ModulesListHeader: React.FC = observer(() => { // placement="bottom-start" customButton={ - {modulesView === 'gantt_chart' ? : modulesView === 'grid' ? : } + {modulesView === "gantt_chart" ? ( + + ) : modulesView === "grid" ? ( + + ) : ( + + )} Layout } @@ -115,6 +124,7 @@ export const ModulesListHeader: React.FC = observer(() => { > {MODULE_VIEW_LAYOUTS.map((layout) => ( setModulesView(layout.key)} className="flex items-center gap-2" > @@ -127,5 +137,3 @@ export const ModulesListHeader: React.FC = observer(() => {
    ); }); - - diff --git a/web/components/headers/page-details.tsx b/web/components/headers/page-details.tsx index e2a427db7..0eed72178 100644 --- a/web/components/headers/page-details.tsx +++ b/web/components/headers/page-details.tsx @@ -1,16 +1,16 @@ import { FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { FileText, Plus } from "lucide-react"; // hooks -import { useApplication, usePage, useProject } from "hooks/store"; // ui import { Breadcrumbs, Button } from "@plane/ui"; // helpers +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { renderEmoji } from "helpers/emoji.helper"; // components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; +import { useApplication, usePage, useProject } from "hooks/store"; export interface IPagesHeaderProps { showButton?: boolean; diff --git a/web/components/headers/pages.tsx b/web/components/headers/pages.tsx index 1984971d6..b5ce74fc5 100644 --- a/web/components/headers/pages.tsx +++ b/web/components/headers/pages.tsx @@ -1,17 +1,17 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { FileText, Plus } from "lucide-react"; // hooks -import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; // ui import { Breadcrumbs, Button } from "@plane/ui"; // helpers +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { EUserProjectRoles } from "constants/project"; import { renderEmoji } from "helpers/emoji.helper"; // constants -import { EUserProjectRoles } from "constants/project"; // components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; +import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; export const PagesHeader = observer(() => { // router diff --git a/web/components/headers/profile-settings.tsx b/web/components/headers/profile-settings.tsx index 24c69f093..5c419f05b 100644 --- a/web/components/headers/profile-settings.tsx +++ b/web/components/headers/profile-settings.tsx @@ -1,7 +1,7 @@ import { FC } from "react"; // ui -import { Breadcrumbs } from "@plane/ui"; import { Settings } from "lucide-react"; +import { Breadcrumbs } from "@plane/ui"; import { BreadcrumbLink } from "components/common"; interface IProfileSettingHeader { diff --git a/web/components/headers/project-archived-issue-details.tsx b/web/components/headers/project-archived-issue-details.tsx index 9d4596f83..8752e7396 100644 --- a/web/components/headers/project-archived-issue-details.tsx +++ b/web/components/headers/project-archived-issue-details.tsx @@ -1,22 +1,22 @@ import { FC } from "react"; -import useSWR from "swr"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; +import useSWR from "swr"; // hooks +import { Breadcrumbs, LayersIcon } from "@plane/ui"; +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { ISSUE_DETAILS } from "constants/fetch-keys"; +import { renderEmoji } from "helpers/emoji.helper"; import { useProject } from "hooks/store"; // ui -import { Breadcrumbs, LayersIcon } from "@plane/ui"; // types +import { IssueArchiveService } from "services/issue"; import { TIssue } from "@plane/types"; // constants -import { ISSUE_DETAILS } from "constants/fetch-keys"; // services -import { IssueArchiveService } from "services/issue"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; // components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; const issueArchiveService = new IssueArchiveService(); diff --git a/web/components/headers/project-archived-issues.tsx b/web/components/headers/project-archived-issues.tsx index d1da1c859..8ade61aae 100644 --- a/web/components/headers/project-archived-issues.tsx +++ b/web/components/headers/project-archived-issues.tsx @@ -1,19 +1,19 @@ import { FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { ArrowLeft } from "lucide-react"; // hooks -import { useIssues, useLabel, useMember, useProject, useProjectState } from "hooks/store"; // constants -import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; // ui import { Breadcrumbs, LayersIcon } from "@plane/ui"; // components -import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues"; +import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; // helpers import { renderEmoji } from "helpers/emoji.helper"; +import { useIssues, useLabel, useMember, useProject, useProjectState } from "hooks/store"; // types import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; diff --git a/web/components/headers/project-draft-issues.tsx b/web/components/headers/project-draft-issues.tsx index 139ec0257..3fd0cb399 100644 --- a/web/components/headers/project-draft-issues.tsx +++ b/web/components/headers/project-draft-issues.tsx @@ -1,17 +1,17 @@ import { FC, useCallback } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks -import { useIssues, useLabel, useMember, useProject, useProjectState } from "hooks/store"; // components -import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; -// ui import { Breadcrumbs, LayersIcon } from "@plane/ui"; +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; +// ui // helper -import { renderEmoji } from "helpers/emoji.helper"; import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { renderEmoji } from "helpers/emoji.helper"; +import { useIssues, useLabel, useMember, useProject, useProjectState } from "hooks/store"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; export const ProjectDraftIssueHeader: FC = observer(() => { diff --git a/web/components/headers/project-inbox.tsx b/web/components/headers/project-inbox.tsx index b5260edd7..b89fbaaac 100644 --- a/web/components/headers/project-inbox.tsx +++ b/web/components/headers/project-inbox.tsx @@ -1,17 +1,17 @@ import { FC, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Plus } from "lucide-react"; // hooks -import { useProject } from "hooks/store"; // ui import { Breadcrumbs, Button, LayersIcon } from "@plane/ui"; // components -import { CreateInboxIssueModal } from "components/inbox"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { CreateInboxIssueModal } from "components/inbox"; // helper import { renderEmoji } from "helpers/emoji.helper"; +import { useProject } from "hooks/store"; export const ProjectInboxHeader: FC = observer(() => { // states diff --git a/web/components/headers/project-issue-details.tsx b/web/components/headers/project-issue-details.tsx index 3732f2598..2f6349e61 100644 --- a/web/components/headers/project-issue-details.tsx +++ b/web/components/headers/project-issue-details.tsx @@ -1,22 +1,22 @@ import { FC } from "react"; -import useSWR from "swr"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; +import useSWR from "swr"; // hooks +import { PanelRight } from "lucide-react"; +import { Breadcrumbs, LayersIcon } from "@plane/ui"; +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { ISSUE_DETAILS } from "constants/fetch-keys"; +import { cn } from "helpers/common.helper"; +import { renderEmoji } from "helpers/emoji.helper"; import { useApplication, useProject } from "hooks/store"; // ui -import { Breadcrumbs, LayersIcon } from "@plane/ui"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; // services import { IssueService } from "services/issue"; // constants -import { ISSUE_DETAILS } from "constants/fetch-keys"; // components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; -import { PanelRight } from "lucide-react"; -import { cn } from "helpers/common.helper"; // services const issueService = new IssueService(); @@ -91,7 +91,9 @@ export const ProjectIssueDetailsHeader: FC = observer(() => {
    ); diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx index 43030c5c2..8e8807fdb 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/components/headers/project-issues.tsx @@ -1,8 +1,17 @@ import { useCallback, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Briefcase, Circle, ExternalLink, Plus } from "lucide-react"; // hooks +import { Breadcrumbs, Button, LayersIcon } from "@plane/ui"; +import { ProjectAnalyticsModal } from "components/analytics"; +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; +import { IssuesMobileHeader } from "components/issues/issues-mobile-header"; +import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { renderEmoji } from "helpers/emoji.helper"; import { useApplication, useEventTracker, @@ -13,21 +22,12 @@ import { useMember, } from "hooks/store"; // components -import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; -import { ProjectAnalyticsModal } from "components/analytics"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; // ui -import { Breadcrumbs, Button, LayersIcon } from "@plane/ui"; // types +import { useIssues } from "hooks/store/use-issues"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; // constants -import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; // helper -import { renderEmoji } from "helpers/emoji.helper"; -import { EUserProjectRoles } from "constants/project"; -import { useIssues } from "hooks/store/use-issues"; -import { IssuesMobileHeader } from "components/issues/issues-mobile-header"; export const ProjectIssuesHeader: React.FC = observer(() => { // states diff --git a/web/components/headers/project-settings.tsx b/web/components/headers/project-settings.tsx index b70a4614f..87b2e507e 100644 --- a/web/components/headers/project-settings.tsx +++ b/web/components/headers/project-settings.tsx @@ -1,17 +1,17 @@ import { FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // ui import { Breadcrumbs, CustomMenu } from "@plane/ui"; // helper +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "constants/project"; import { renderEmoji } from "helpers/emoji.helper"; // hooks import { useProject, useUser } from "hooks/store"; // constants -import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "constants/project"; // components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; export interface IProjectSettingHeader { title: string; diff --git a/web/components/headers/project-view-issues.tsx b/web/components/headers/project-view-issues.tsx index 175534a79..eea211431 100644 --- a/web/components/headers/project-view-issues.tsx +++ b/web/components/headers/project-view-issues.tsx @@ -1,9 +1,22 @@ import { useCallback } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { Plus } from "lucide-react"; import Link from "next/link"; +import { useRouter } from "next/router"; +import { Plus } from "lucide-react"; // hooks +// components +// ui +import { Breadcrumbs, Button, CustomMenu, PhotoFilterIcon } from "@plane/ui"; +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; +// helpers +// types +// constants +import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { renderEmoji } from "helpers/emoji.helper"; +import { truncateText } from "helpers/string.helper"; import { useApplication, useEventTracker, @@ -15,20 +28,7 @@ import { useProjectView, useUser, } from "hooks/store"; -// components -import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; -// ui -import { Breadcrumbs, Button, CustomMenu, PhotoFilterIcon } from "@plane/ui"; -// helpers -import { truncateText } from "helpers/string.helper"; -import { renderEmoji } from "helpers/emoji.helper"; -// types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; -// constants -import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; export const ProjectViewIssuesHeader: React.FC = observer(() => { // router diff --git a/web/components/headers/project-views.tsx b/web/components/headers/project-views.tsx index bb070a22f..3b4d7fb20 100644 --- a/web/components/headers/project-views.tsx +++ b/web/components/headers/project-views.tsx @@ -1,16 +1,16 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Plus } from "lucide-react"; // hooks -import { useApplication, useProject, useUser } from "hooks/store"; // components import { Breadcrumbs, PhotoFilterIcon, Button } from "@plane/ui"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; // helpers +import { EUserProjectRoles } from "constants/project"; import { renderEmoji } from "helpers/emoji.helper"; // constants -import { EUserProjectRoles } from "constants/project"; +import { useApplication, useProject, useUser } from "hooks/store"; export const ProjectViewsHeader: React.FC = observer(() => { // router diff --git a/web/components/headers/projects.tsx b/web/components/headers/projects.tsx index f6dd7fd3c..3810860aa 100644 --- a/web/components/headers/projects.tsx +++ b/web/components/headers/projects.tsx @@ -1,14 +1,14 @@ import { observer } from "mobx-react-lite"; import { Search, Plus, Briefcase } from "lucide-react"; // hooks -import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; // ui import { Breadcrumbs, Button } from "@plane/ui"; // constants +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { EUserWorkspaceRoles } from "constants/workspace"; // components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; +import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; export const ProjectsHeader = observer(() => { // store hooks diff --git a/web/components/headers/user-profile.tsx b/web/components/headers/user-profile.tsx index 30bc5b2a9..09b764cdc 100644 --- a/web/components/headers/user-profile.tsx +++ b/web/components/headers/user-profile.tsx @@ -1,23 +1,23 @@ // ui +import { FC } from "react"; +import { observer } from "mobx-react-lite"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { ChevronDown, PanelRight } from "lucide-react"; import { Breadcrumbs, CustomMenu } from "@plane/ui"; import { BreadcrumbLink } from "components/common"; // components import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { cn } from "helpers/common.helper"; -import { FC } from "react"; -import { useApplication, useUser } from "hooks/store"; -import { ChevronDown, PanelRight } from "lucide-react"; -import { observer } from "mobx-react-lite"; import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "constants/profile"; -import Link from "next/link"; -import { useRouter } from "next/router"; +import { cn } from "helpers/common.helper"; +import { useApplication, useUser } from "hooks/store"; type TUserProfileHeader = { - type?: string | undefined -} + type?: string | undefined; +}; export const UserProfileHeader: FC = observer((props) => { - const { type = undefined } = props + const { type = undefined } = props; const router = useRouter(); const { workspaceSlug, userId } = router.query; @@ -34,45 +34,60 @@ export const UserProfileHeader: FC = observer((props) => { const { theme: themStore } = useApplication(); - return (
    -
    - -
    - - } /> - -
    - - {type} - -
    - } - customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm" - closeOnSelect - > - <> - {tabsList.map((tab) => ( - - {tab.label} - - ))} - - + return ( +
    +
    + +
    + + } + /> + +
    + + {type} + +
    + } + customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm" + closeOnSelect + > + <> + {tabsList.map((tab) => ( + + + {tab.label} + + + ))} + + +
    -
    ) + ); }); - - diff --git a/web/components/headers/workspace-active-cycles.tsx b/web/components/headers/workspace-active-cycles.tsx index 195b89471..a33161de9 100644 --- a/web/components/headers/workspace-active-cycles.tsx +++ b/web/components/headers/workspace-active-cycles.tsx @@ -1,10 +1,10 @@ import { observer } from "mobx-react-lite"; // ui +import { Crown } from "lucide-react"; import { Breadcrumbs, ContrastIcon } from "@plane/ui"; import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; // icons -import { Crown } from "lucide-react"; export const WorkspaceActiveCycleHeader = observer(() => (
    diff --git a/web/components/headers/workspace-analytics.tsx b/web/components/headers/workspace-analytics.tsx index a6ad67f05..2bede32ba 100644 --- a/web/components/headers/workspace-analytics.tsx +++ b/web/components/headers/workspace-analytics.tsx @@ -1,14 +1,14 @@ +import { useEffect } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import { BarChart2, PanelRight } from "lucide-react"; // ui import { Breadcrumbs } from "@plane/ui"; // components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { BreadcrumbLink } from "components/common"; -import { useApplication } from "hooks/store"; -import { observer } from "mobx-react"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { cn } from "helpers/common.helper"; -import { useEffect } from "react"; +import { useApplication } from "hooks/store"; export const WorkspaceAnalyticsHeader = observer(() => { const router = useRouter(); @@ -47,11 +47,21 @@ export const WorkspaceAnalyticsHeader = observer(() => { } /> - {analytics_tab === 'custom' && - - } + )}
    diff --git a/web/components/headers/workspace-dashboard.tsx b/web/components/headers/workspace-dashboard.tsx index 6b85577f6..e7ae3c726 100644 --- a/web/components/headers/workspace-dashboard.tsx +++ b/web/components/headers/workspace-dashboard.tsx @@ -1,17 +1,17 @@ -import { LayoutGrid, Zap } from "lucide-react"; import Image from "next/image"; import { useTheme } from "next-themes"; +import { LayoutGrid, Zap } from "lucide-react"; // images import githubBlackImage from "/public/logos/github-black.png"; import githubWhiteImage from "/public/logos/github-white.png"; // hooks -import { useEventTracker } from "hooks/store"; // components -import { BreadcrumbLink } from "components/common"; import { Breadcrumbs } from "@plane/ui"; +import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; // constants import { CHANGELOG_REDIRECTED, GITHUB_REDIRECTED } from "constants/event-tracker"; +import { useEventTracker } from "hooks/store"; export const WorkspaceDashboardHeader = () => { // hooks diff --git a/web/components/headers/workspace-settings.tsx b/web/components/headers/workspace-settings.tsx index 5ced55204..faf1a45d1 100644 --- a/web/components/headers/workspace-settings.tsx +++ b/web/components/headers/workspace-settings.tsx @@ -1,14 +1,14 @@ import { FC } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // ui -import { Breadcrumbs, CustomMenu } from "@plane/ui"; import { Settings } from "lucide-react"; +import { Breadcrumbs, CustomMenu } from "@plane/ui"; // hooks -import { observer } from "mobx-react-lite"; // components +import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { WORKSPACE_SETTINGS_LINKS } from "constants/workspace"; -import { BreadcrumbLink } from "components/common"; export interface IWorkspaceSettingHeader { title: string; diff --git a/web/components/icons/priority-icon.tsx b/web/components/icons/priority-icon.tsx index b23f56eab..36ea67b3d 100644 --- a/web/components/icons/priority-icon.tsx +++ b/web/components/icons/priority-icon.tsx @@ -14,12 +14,12 @@ export const PriorityIcon: React.FC = ({ priority, className = "" }) => { {priority === "urgent" ? "error" : priority === "high" - ? "signal_cellular_alt" - : priority === "medium" - ? "signal_cellular_alt_2_bar" - : priority === "low" - ? "signal_cellular_alt_1_bar" - : "block"} + ? "signal_cellular_alt" + : priority === "medium" + ? "signal_cellular_alt_2_bar" + : priority === "low" + ? "signal_cellular_alt_1_bar" + : "block"} ); }; diff --git a/web/components/icons/state/state-group-icon.tsx b/web/components/icons/state/state-group-icon.tsx index 15debf5f2..ae9e5f1a9 100644 --- a/web/components/icons/state/state-group-icon.tsx +++ b/web/components/icons/state/state-group-icon.tsx @@ -7,9 +7,9 @@ import { StateGroupUnstartedIcon, } from "components/icons"; // types +import { STATE_GROUPS } from "constants/state"; import { TStateGroups } from "@plane/types"; // constants -import { STATE_GROUPS } from "constants/state"; type Props = { className?: string; diff --git a/web/components/inbox/content/root.tsx b/web/components/inbox/content/root.tsx index 26f58131e..7cc19bec3 100644 --- a/web/components/inbox/content/root.tsx +++ b/web/components/inbox/content/root.tsx @@ -2,12 +2,12 @@ import { FC } from "react"; import { observer } from "mobx-react"; import { Inbox } from "lucide-react"; // hooks -import { useInboxIssues } from "hooks/store"; -// components +import { Loader } from "@plane/ui"; import { InboxIssueActionsHeader } from "components/inbox"; import { InboxIssueDetailRoot } from "components/issues/issue-detail/inbox"; +import { useInboxIssues } from "hooks/store"; +// components // ui -import { Loader } from "@plane/ui"; type TInboxContentRoot = { workspaceSlug: string; diff --git a/web/components/inbox/inbox-issue-actions.tsx b/web/components/inbox/inbox-issue-actions.tsx index 4d4cfa0cc..661bc2d72 100644 --- a/web/components/inbox/inbox-issue-actions.tsx +++ b/web/components/inbox/inbox-issue-actions.tsx @@ -1,10 +1,12 @@ import { FC, useCallback, useEffect, useMemo, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { DayPicker } from "react-day-picker"; import { Popover } from "@headlessui/react"; -// hooks -import { useUser, useInboxIssues, useIssueDetail, useWorkspace, useEventTracker } from "hooks/store"; +// icons +import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Trash2, XCircle } from "lucide-react"; +// ui +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // components import { AcceptIssueModal, @@ -12,14 +14,12 @@ import { DeleteInboxIssueModal, SelectDuplicateInboxIssueModal, } from "components/inbox"; -// ui -import { Button, TOAST_TYPE, setToast } from "@plane/ui"; -// icons -import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Trash2, XCircle } from "lucide-react"; +import { ISSUE_DELETED } from "constants/event-tracker"; +import { EUserProjectRoles } from "constants/project"; +// hooks +import { useUser, useInboxIssues, useIssueDetail, useWorkspace, useEventTracker } from "hooks/store"; // types import type { TInboxDetailedStatus } from "@plane/types"; -import { EUserProjectRoles } from "constants/project"; -import { ISSUE_DELETED } from "constants/event-tracker"; type TInboxIssueActionsHeader = { workspaceSlug: string; @@ -232,7 +232,7 @@ export const InboxIssueActionsHeader: FC = observer((p )} {inboxIssueId && ( -
    +
    diff --git a/web/components/issues/attachment/attachment-detail.tsx b/web/components/issues/attachment/attachment-detail.tsx index 0d345a619..8ff2b9305 100644 --- a/web/components/issues/attachment/attachment-detail.tsx +++ b/web/components/issues/attachment/attachment-detail.tsx @@ -2,17 +2,17 @@ import { FC, useState } from "react"; import Link from "next/link"; import { AlertCircle, X } from "lucide-react"; // hooks -import { useIssueDetail, useMember } from "hooks/store"; // ui import { Tooltip } from "@plane/ui"; // components -import { IssueAttachmentDeleteModal } from "./delete-attachment-confirmation-modal"; // icons import { getFileIcon } from "components/icons"; // helper -import { truncateText } from "helpers/string.helper"; -import { renderFormattedDate } from "helpers/date-time.helper"; import { convertBytesToSize, getFileExtension, getFileName } from "helpers/attachment.helper"; +import { renderFormattedDate } from "helpers/date-time.helper"; +import { truncateText } from "helpers/string.helper"; +import { useIssueDetail, useMember } from "hooks/store"; +import { IssueAttachmentDeleteModal } from "./delete-attachment-confirmation-modal"; // types import { TAttachmentOperations } from "./root"; diff --git a/web/components/issues/attachment/attachment-upload.tsx b/web/components/issues/attachment/attachment-upload.tsx index bf197980a..27dc572a9 100644 --- a/web/components/issues/attachment/attachment-upload.tsx +++ b/web/components/issues/attachment/attachment-upload.tsx @@ -2,11 +2,11 @@ import { useCallback, useState } from "react"; import { observer } from "mobx-react-lite"; import { useDropzone } from "react-dropzone"; // hooks -import { useApplication } from "hooks/store"; // constants import { MAX_FILE_SIZE } from "constants/common"; // helpers import { generateFileName } from "helpers/attachment.helper"; +import { useApplication } from "hooks/store"; // types import { TAttachmentOperations } from "./root"; diff --git a/web/components/issues/attachment/attachments-list.tsx b/web/components/issues/attachment/attachments-list.tsx index 2129a4f61..0f834c1a4 100644 --- a/web/components/issues/attachment/attachments-list.tsx +++ b/web/components/issues/attachment/attachments-list.tsx @@ -32,6 +32,7 @@ export const IssueAttachmentsList: FC = observer((props) issueAttachments.length > 0 && issueAttachments.map((attachmentId) => ( = observer((props) => { debouncedFormSave(); }} required - className="min-h-min block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-2xl font-medium outline-none ring-0 focus:ring-1 focus:ring-custom-primary" + className="block min-h-min w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-2xl font-medium outline-none ring-0 focus:ring-1 focus:ring-custom-primary" hasError={Boolean(errors?.name)} role="textbox" /> @@ -173,7 +173,7 @@ export const IssueDescriptionForm: FC = observer((props) => { setIsSubmitting={setIsSubmitting} dragDropEnabled customClassName="min-h-[150px] shadow-sm" - onChange={(description: Object, description_html: string) => { + onChange={(description: any, description_html: string) => { setShowAlert(true); setIsSubmitting("submitting"); onChange(description_html); diff --git a/web/components/issues/description-input.tsx b/web/components/issues/description-input.tsx index 65e82df5f..4f1f5c056 100644 --- a/web/components/issues/description-input.tsx +++ b/web/components/issues/description-input.tsx @@ -1,16 +1,15 @@ import { FC, useState, useEffect } from "react"; // components -import { Loader } from "@plane/ui"; import { RichReadOnlyEditor, RichTextEditor } from "@plane/rich-text-editor"; -// store hooks +import { Loader } from "@plane/ui"; +// hooks import { useMention, useWorkspace } from "hooks/store"; +import useDebounce from "hooks/use-debounce"; // services import { FileService } from "services/file.service"; const fileService = new FileService(); // types import { TIssueOperations } from "./issue-detail"; -// hooks -import useDebounce from "hooks/use-debounce"; export type IssueDescriptionInputProps = { workspaceSlug: string; @@ -78,7 +77,7 @@ export const IssueDescriptionInput: FC = (props) => initialValue={initialValue} dragDropEnabled customClassName="min-h-[150px] shadow-sm" - onChange={(description: Object, description_html: string) => { + onChange={(description: any, description_html: string) => { setIsSubmitting("submitting"); setDescriptionHTML(description_html === "" ? "

    " : description_html); }} diff --git a/web/components/issues/issue-detail/cycle-select.tsx b/web/components/issues/issue-detail/cycle-select.tsx index 4da762a9d..8744857c1 100644 --- a/web/components/issues/issue-detail/cycle-select.tsx +++ b/web/components/issues/issue-detail/cycle-select.tsx @@ -1,13 +1,12 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; // hooks -import { useIssueDetail } from "hooks/store"; // components import { CycleDropdown } from "components/dropdowns"; // ui -import { Spinner } from "@plane/ui"; // helpers import { cn } from "helpers/common.helper"; +import { useIssueDetail } from "hooks/store"; // types import type { TIssueOperations } from "./root"; @@ -41,14 +40,14 @@ export const IssueCycleSelect: React.FC = observer((props) => }; return ( -
    +
    = observer((props) => { projectId={projectId} inboxId={inboxId} issueId={issueId} - showDescription={true} + showDescription />
    diff --git a/web/components/issues/issue-detail/inbox/root.tsx b/web/components/issues/issue-detail/inbox/root.tsx index 9b0e961c0..144198085 100644 --- a/web/components/issues/issue-detail/inbox/root.tsx +++ b/web/components/issues/issue-detail/inbox/root.tsx @@ -2,17 +2,16 @@ import { FC, useMemo } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; // components -import { InboxIssueMainContent } from "./main-content"; -import { InboxIssueDetailsSidebar } from "./sidebar"; -// hooks +import { TOAST_TYPE, setToast } from "@plane/ui"; +import { EUserProjectRoles } from "constants/project"; import { useEventTracker, useInboxIssues, useIssueDetail, useUser } from "hooks/store"; // ui -import { TOAST_TYPE, setToast } from "@plane/ui"; // types import { TIssue } from "@plane/types"; import { TIssueOperations } from "../root"; +import { InboxIssueMainContent } from "./main-content"; +import { InboxIssueDetailsSidebar } from "./sidebar"; // constants -import { EUserProjectRoles } from "constants/project"; export type TInboxIssueDetailRoot = { workspaceSlug: string; @@ -48,12 +47,7 @@ export const InboxIssueDetailRoot: FC = (props) => { console.error("Error fetching the parent issue"); } }, - update: async ( - workspaceSlug: string, - projectId: string, - issueId: string, - data: Partial, - ) => { + update: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => { try { await updateInboxIssue(workspaceSlug, projectId, inboxId, issueId, data); captureIssueEvent({ diff --git a/web/components/issues/issue-detail/inbox/sidebar.tsx b/web/components/issues/issue-detail/inbox/sidebar.tsx index 592791a85..bf9e833ce 100644 --- a/web/components/issues/issue-detail/inbox/sidebar.tsx +++ b/web/components/issues/issue-detail/inbox/sidebar.tsx @@ -2,14 +2,14 @@ import React from "react"; import { observer } from "mobx-react-lite"; import { CalendarCheck2, Signal, Tag } from "lucide-react"; // hooks -import { useIssueDetail, useProject, useProjectState } from "hooks/store"; // components -import { IssueLabel, TIssueOperations } from "components/issues"; -import { DateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns"; -// icons import { DoubleCircleIcon, StateGroupIcon, UserGroupIcon } from "@plane/ui"; +import { DateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns"; +import { IssueLabel, TIssueOperations } from "components/issues"; +// icons // helper import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { useIssueDetail, useProject, useProjectState } from "hooks/store"; type Props = { workspaceSlug: string; diff --git a/web/components/issues/issue-detail/issue-activity/activity-comment-root.tsx b/web/components/issues/issue-detail/issue-activity/activity-comment-root.tsx index 575e8d841..af3266067 100644 --- a/web/components/issues/issue-detail/issue-activity/activity-comment-root.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity-comment-root.tsx @@ -31,6 +31,7 @@ export const IssueActivityCommentRoot: FC = observer( {activityComments.map((activityComment, index) => activityComment.activity_type === "COMMENT" ? ( = observer((props > <> {activity.old_value === "" ? `added a new assignee ` : `removed the assignee `} - = observer((props > {activity.new_value && activity.new_value !== "" ? activity.new_value : activity.old_value} - {showIssue && (activity.old_value === "" ? ` to ` : ` from `)} {showIssue && }. diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/cycle.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/cycle.tsx index 8336e516f..ec3c777fc 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/cycle.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/cycle.tsx @@ -1,11 +1,11 @@ import { FC } from "react"; import { observer } from "mobx-react"; // hooks +import { ContrastIcon } from "@plane/ui"; import { useIssueDetail } from "hooks/store"; // components import { IssueActivityBlockComponent } from "./"; // icons -import { ContrastIcon } from "@plane/ui"; type TIssueCycleActivity = { activityId: string; ends: "top" | "bottom" | undefined }; diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/default.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/default.tsx index e45387535..0eeb7ecac 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/default.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/default.tsx @@ -1,11 +1,11 @@ import { FC } from "react"; import { observer } from "mobx-react"; // hooks +import { LayersIcon } from "@plane/ui"; import { useIssueDetail } from "hooks/store"; // components import { IssueActivityBlockComponent } from "./"; // icons -import { LayersIcon } from "@plane/ui"; type TIssueDefaultActivity = { activityId: string; ends: "top" | "bottom" | undefined }; diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/estimate.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/estimate.tsx index e01b94e1b..a8c309bd5 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/estimate.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/estimate.tsx @@ -33,13 +33,11 @@ export const IssueEstimateActivity: FC = observer((props {activity.new_value ? `set the estimate point to ` : `removed the estimate point `} {activity.new_value && ( <> - {areEstimatesEnabledForCurrentProject ? estimateValue : `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`} - )} {showIssue && (activity.new_value ? ` to ` : ` from `)} diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx index e209b4bbf..0097b65b6 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx @@ -1,13 +1,13 @@ import { FC, ReactNode } from "react"; import { Network } from "lucide-react"; // hooks +import { Tooltip } from "@plane/ui"; +import { renderFormattedTime, renderFormattedDate, calculateTimeAgo } from "helpers/date-time.helper"; import { useIssueDetail } from "hooks/store"; // ui -import { Tooltip } from "@plane/ui"; // components import { IssueUser } from "../"; // helpers -import { renderFormattedTime, renderFormattedDate, calculateTimeAgo } from "helpers/date-time.helper"; type TIssueActivityBlockComponent = { icon?: ReactNode; @@ -33,7 +33,7 @@ export const IssueActivityBlockComponent: FC = (pr ends === "top" ? `pb-2` : ends === "bottom" ? `pt-2` : `py-2` }`} > -
    +
    {icon ? icon : }
    diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-link.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-link.tsx index e86b1fb57..49f813ec6 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-link.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-link.tsx @@ -1,8 +1,8 @@ import { FC } from "react"; // hooks +import { Tooltip } from "@plane/ui"; import { useIssueDetail } from "hooks/store"; // ui -import { Tooltip } from "@plane/ui"; type TIssueLink = { activityId: string; diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/module.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/module.tsx index c8089d233..0108c56b3 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/module.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/module.tsx @@ -1,11 +1,11 @@ import { FC } from "react"; import { observer } from "mobx-react"; // hooks +import { DiceIcon } from "@plane/ui"; import { useIssueDetail } from "hooks/store"; // components import { IssueActivityBlockComponent } from "./"; // icons -import { DiceIcon } from "@plane/ui"; type TIssueModuleActivity = { activityId: string; ends: "top" | "bottom" | undefined }; diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/relation.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/relation.tsx index e68a7c373..5ef67cf52 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/relation.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/relation.tsx @@ -1,13 +1,13 @@ import { FC } from "react"; import { observer } from "mobx-react"; // hooks +import { issueRelationObject } from "components/issues/issue-detail/relation-select"; import { useIssueDetail } from "hooks/store"; // components +import { TIssueRelationTypes } from "@plane/types"; import { IssueActivityBlockComponent } from "./"; // component helpers -import { issueRelationObject } from "components/issues/issue-detail/relation-select"; // types -import { TIssueRelationTypes } from "@plane/types"; type TIssueRelationActivity = { activityId: string; ends: "top" | "bottom" | undefined }; diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/start_date.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/start_date.tsx index 95b3cda80..0e3a80b34 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/start_date.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/start_date.tsx @@ -2,11 +2,11 @@ import { FC } from "react"; import { observer } from "mobx-react"; import { CalendarDays } from "lucide-react"; // hooks +import { renderFormattedDate } from "helpers/date-time.helper"; import { useIssueDetail } from "hooks/store"; // components import { IssueActivityBlockComponent, IssueLink } from "./"; // helpers -import { renderFormattedDate } from "helpers/date-time.helper"; type TIssueStartDateActivity = { activityId: string; showIssue?: boolean; ends: "top" | "bottom" | undefined }; diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/state.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/state.tsx index 7cc47c2c8..757519388 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/state.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/state.tsx @@ -1,11 +1,11 @@ import { FC } from "react"; import { observer } from "mobx-react"; // hooks +import { DoubleCircleIcon } from "@plane/ui"; import { useIssueDetail } from "hooks/store"; // components import { IssueActivityBlockComponent, IssueLink } from "./"; // icons -import { DoubleCircleIcon } from "@plane/ui"; type TIssueStateActivity = { activityId: string; showIssue?: boolean; ends: "top" | "bottom" | undefined }; diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/target_date.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/target_date.tsx index a4b40ec31..947b2e6e6 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/target_date.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/target_date.tsx @@ -2,11 +2,11 @@ import { FC } from "react"; import { observer } from "mobx-react"; import { CalendarDays } from "lucide-react"; // hooks +import { renderFormattedDate } from "helpers/date-time.helper"; import { useIssueDetail } from "hooks/store"; // components import { IssueActivityBlockComponent, IssueLink } from "./"; // helpers -import { renderFormattedDate } from "helpers/date-time.helper"; type TIssueTargetDateActivity = { activityId: string; showIssue?: boolean; ends: "top" | "bottom" | undefined }; diff --git a/web/components/issues/issue-detail/issue-activity/activity/root.tsx b/web/components/issues/issue-detail/issue-activity/activity/root.tsx index af44463d5..092633b06 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/root.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/root.tsx @@ -23,6 +23,7 @@ export const IssueActivityRoot: FC = observer((props) => {
    {activityIds.map((activityId, index) => ( diff --git a/web/components/issues/issue-detail/issue-activity/comments/comment-block.tsx b/web/components/issues/issue-detail/issue-activity/comments/comment-block.tsx index 4dbc36f6b..b00dd2a13 100644 --- a/web/components/issues/issue-detail/issue-activity/comments/comment-block.tsx +++ b/web/components/issues/issue-detail/issue-activity/comments/comment-block.tsx @@ -1,9 +1,9 @@ import { FC, ReactNode } from "react"; import { MessageCircle } from "lucide-react"; // hooks +import { calculateTimeAgo } from "helpers/date-time.helper"; import { useIssueDetail } from "hooks/store"; // helpers -import { calculateTimeAgo } from "helpers/date-time.helper"; type TIssueCommentBlock = { commentId: string; @@ -24,7 +24,7 @@ export const IssueCommentBlock: FC = (props) => { if (!comment) return <>; return (
    -
    +
    {comment.actor_detail.avatar && comment.actor_detail.avatar !== "" ? ( = (props) => { value={watch("comment_html") ?? ""} debouncedUpdatesEnabled={false} customClassName="min-h-[50px] p-3 shadow-sm" - onChange={(comment_json: Object, comment_html: string) => setValue("comment_html", comment_html)} + onChange={(comment_json: any, comment_html: string) => setValue("comment_html", comment_html)} mentionSuggestions={mentionSuggestions} mentionHighlights={mentionHighlights} /> @@ -150,7 +150,7 @@ export const IssueCommentCard: FC = (props) => { onClick={handleSubmit(onEnter)} disabled={isSubmitting || isEmpty} className={`group rounded border border-green-500 bg-green-500/20 p-2 shadow-md duration-300 ${ - isEmpty ? "bg-gray-200 cursor-not-allowed" : "hover:bg-green-500" + isEmpty ? "cursor-not-allowed bg-gray-200" : "hover:bg-green-500" }`} > = (props) => { customClassName="p-2" editorContentCustomClassNames="min-h-[35px]" debouncedUpdatesEnabled={false} - onChange={(comment_json: Object, comment_html: string) => { + onChange={(comment_json: any, comment_html: string) => { onChange(comment_html); }} mentionSuggestions={mentionSuggestions} diff --git a/web/components/issues/issue-detail/issue-activity/comments/root.tsx b/web/components/issues/issue-detail/issue-activity/comments/root.tsx index 4e2775c4a..0696fa129 100644 --- a/web/components/issues/issue-detail/issue-activity/comments/root.tsx +++ b/web/components/issues/issue-detail/issue-activity/comments/root.tsx @@ -3,9 +3,9 @@ import { observer } from "mobx-react-lite"; // hooks import { useIssueDetail } from "hooks/store"; // components +import { TActivityOperations } from "../root"; import { IssueCommentCard } from "./comment-card"; // types -import { TActivityOperations } from "../root"; type TIssueCommentRoot = { workspaceSlug: string; @@ -28,6 +28,7 @@ export const IssueCommentRoot: FC = observer((props) => {
    {commentIds.map((commentId, index) => ( = (props) => { return ( <>
    @@ -149,7 +149,7 @@ export const LabelCreate: FC = (props) => { )} diff --git a/web/components/issues/issue-detail/label/label-list-item.tsx b/web/components/issues/issue-detail/label/label-list-item.tsx index 60792b01c..69c0e08e9 100644 --- a/web/components/issues/issue-detail/label/label-list-item.tsx +++ b/web/components/issues/issue-detail/label/label-list-item.tsx @@ -1,8 +1,8 @@ import { FC } from "react"; import { X } from "lucide-react"; // types -import { TLabelOperations } from "./root"; import { useIssueDetail, useLabel } from "hooks/store"; +import { TLabelOperations } from "./root"; type TLabelListItem = { workspaceSlug: string; diff --git a/web/components/issues/issue-detail/label/label-list.tsx b/web/components/issues/issue-detail/label/label-list.tsx index fd714e002..fdf94be28 100644 --- a/web/components/issues/issue-detail/label/label-list.tsx +++ b/web/components/issues/issue-detail/label/label-list.tsx @@ -1,8 +1,8 @@ import { FC } from "react"; // components +import { useIssueDetail } from "hooks/store"; import { LabelListItem } from "./label-list-item"; // hooks -import { useIssueDetail } from "hooks/store"; // types import { TLabelOperations } from "./root"; @@ -29,6 +29,7 @@ export const LabelList: FC = (props) => { <> {issueLabels.map((labelId) => ( = observer((props) => // states const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); - const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(false); const [query, setQuery] = useState(""); const issue = getIssueById(issueId); @@ -71,7 +71,7 @@ export const IssueLabelSelect: React.FC = observer((props) => const label = (
    @@ -102,7 +102,7 @@ export const IssueLabelSelect: React.FC = observer((props) =>
    -
    +
    {isLoading ? (

    Loading...

    ) : filteredOptions.length > 0 ? ( diff --git a/web/components/issues/issue-detail/label/select/root.tsx b/web/components/issues/issue-detail/label/select/root.tsx index c31e1bc61..de0bcca90 100644 --- a/web/components/issues/issue-detail/label/select/root.tsx +++ b/web/components/issues/issue-detail/label/select/root.tsx @@ -1,8 +1,8 @@ import { FC } from "react"; // components +import { TLabelOperations } from "../root"; import { IssueLabelSelect } from "./label-select"; // types -import { TLabelOperations } from "../root"; type TIssueLabelSelectRoot = { workspaceSlug: string; diff --git a/web/components/issues/issue-detail/links/create-update-link-modal.tsx b/web/components/issues/issue-detail/links/create-update-link-modal.tsx index fc9eb3838..689968f07 100644 --- a/web/components/issues/issue-detail/links/create-update-link-modal.tsx +++ b/web/components/issues/issue-detail/links/create-update-link-modal.tsx @@ -152,8 +152,8 @@ export const IssueLinkCreateUpdateModal: FC = (props) ? "Updating Link..." : "Update Link" : isSubmitting - ? "Adding Link..." - : "Add Link"} + ? "Adding Link..." + : "Add Link"}
    diff --git a/web/components/issues/issue-detail/links/link-detail.tsx b/web/components/issues/issue-detail/links/link-detail.tsx index f1b003b99..4504329f0 100644 --- a/web/components/issues/issue-detail/links/link-detail.tsx +++ b/web/components/issues/issue-detail/links/link-detail.tsx @@ -1,15 +1,15 @@ import { FC, useState } from "react"; // hooks -import { useIssueDetail, useMember } from "hooks/store"; // ui +import { Pencil, Trash2, LinkIcon } from "lucide-react"; import { ExternalLinkIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // icons -import { Pencil, Trash2, LinkIcon } from "lucide-react"; // types -import { IssueLinkCreateUpdateModal, TLinkOperationsModal } from "./create-update-link-modal"; // helpers import { calculateTimeAgo } from "helpers/date-time.helper"; import { copyTextToClipboard } from "helpers/string.helper"; +import { useIssueDetail, useMember } from "hooks/store"; +import { IssueLinkCreateUpdateModal, TLinkOperationsModal } from "./create-update-link-modal"; export type TIssueLinkDetail = { linkId: string; @@ -50,7 +50,7 @@ export const IssueLinkDetail: FC = (props) => {
    { copyTextToClipboard(linkDetail.url); setToast({ diff --git a/web/components/issues/issue-detail/links/links.tsx b/web/components/issues/issue-detail/links/links.tsx index 368bddb91..1120c3a5c 100644 --- a/web/components/issues/issue-detail/links/links.tsx +++ b/web/components/issues/issue-detail/links/links.tsx @@ -1,9 +1,9 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // computed +import { useIssueDetail, useUser } from "hooks/store"; import { IssueLinkDetail } from "./link-detail"; // hooks -import { useIssueDetail, useUser } from "hooks/store"; import { TLinkOperations } from "./root"; export type TLinkOperationsModal = Exclude; @@ -34,6 +34,7 @@ export const IssueLinkList: FC = observer((props) => { issueLinks.length > 0 && issueLinks.map((linkId) => ( ) => Promise; diff --git a/web/components/issues/issue-detail/main-content.tsx b/web/components/issues/issue-detail/main-content.tsx index 719129d98..b65560953 100644 --- a/web/components/issues/issue-detail/main-content.tsx +++ b/web/components/issues/issue-detail/main-content.tsx @@ -1,18 +1,18 @@ import { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; // hooks +import { StateGroupIcon } from "@plane/ui"; +import { IssueAttachmentRoot, IssueUpdateStatus } from "components/issues"; import { useIssueDetail, useProjectState, useUser } from "hooks/store"; import useReloadConfirmations from "hooks/use-reload-confirmation"; // components -import { IssueAttachmentRoot, IssueUpdateStatus } from "components/issues"; -import { IssueTitleInput } from "../title-input"; import { IssueDescriptionInput } from "../description-input"; +import { SubIssuesRoot } from "../sub-issues"; +import { IssueTitleInput } from "../title-input"; +import { IssueActivity } from "./issue-activity"; import { IssueParentDetail } from "./parent"; import { IssueReaction } from "./reactions"; -import { SubIssuesRoot } from "../sub-issues"; -import { IssueActivity } from "./issue-activity"; // ui -import { StateGroupIcon } from "@plane/ui"; // types import { TIssueOperations } from "./root"; diff --git a/web/components/issues/issue-detail/module-select.tsx b/web/components/issues/issue-detail/module-select.tsx index f0fe06a2e..f157ede86 100644 --- a/web/components/issues/issue-detail/module-select.tsx +++ b/web/components/issues/issue-detail/module-select.tsx @@ -1,14 +1,13 @@ import React, { useState } from "react"; -import { observer } from "mobx-react-lite"; import xor from "lodash/xor"; +import { observer } from "mobx-react-lite"; // hooks -import { useIssueDetail } from "hooks/store"; // components import { ModuleDropdown } from "components/dropdowns"; // ui -import { Spinner } from "@plane/ui"; // helpers import { cn } from "helpers/common.helper"; +import { useIssueDetail } from "hooks/store"; // types import type { TIssueOperations } from "./root"; @@ -58,14 +57,14 @@ export const IssueModuleSelect: React.FC = observer((props) }; return ( -
    +
    = (props) => { Loading
    ) : subIssueIds && subIssueIds.length > 0 ? ( - subIssueIds.map((issueId) => currentIssue.id != issueId && ) + subIssueIds.map( + (issueId) => currentIssue.id != issueId && + ) ) : (
    No sibling issues diff --git a/web/components/issues/issue-detail/reactions/issue-comment.tsx b/web/components/issues/issue-detail/reactions/issue-comment.tsx index 2268540bf..97c63a017 100644 --- a/web/components/issues/issue-detail/reactions/issue-comment.tsx +++ b/web/components/issues/issue-detail/reactions/issue-comment.tsx @@ -1,14 +1,13 @@ import { FC, useMemo } from "react"; import { observer } from "mobx-react-lite"; // components -import { ReactionSelector } from "./reaction-selector"; -// hooks +import { TOAST_TYPE, setToast } from "@plane/ui"; +import { renderEmoji } from "helpers/emoji.helper"; import { useIssueDetail } from "hooks/store"; // ui -import { TOAST_TYPE, setToast } from "@plane/ui"; // types import { IUser } from "@plane/types"; -import { renderEmoji } from "helpers/emoji.helper"; +import { ReactionSelector } from "./reaction-selector"; export type TIssueCommentReaction = { workspaceSlug: string; @@ -71,15 +70,7 @@ export const IssueCommentReaction: FC = observer((props) else await issueCommentReactionOperations.create(reaction); }, }), - [ - workspaceSlug, - projectId, - commentId, - currentUser, - createCommentReaction, - removeCommentReaction, - userReactions, - ] + [workspaceSlug, projectId, commentId, currentUser, createCommentReaction, removeCommentReaction, userReactions] ); return ( diff --git a/web/components/issues/issue-detail/reactions/issue.tsx b/web/components/issues/issue-detail/reactions/issue.tsx index a9bc264f3..6f5610634 100644 --- a/web/components/issues/issue-detail/reactions/issue.tsx +++ b/web/components/issues/issue-detail/reactions/issue.tsx @@ -1,14 +1,13 @@ import { FC, useMemo } from "react"; import { observer } from "mobx-react-lite"; // components -import { ReactionSelector } from "./reaction-selector"; -// hooks +import { TOAST_TYPE, setToast } from "@plane/ui"; +import { renderEmoji } from "helpers/emoji.helper"; import { useIssueDetail } from "hooks/store"; // ui -import { TOAST_TYPE, setToast } from "@plane/ui"; // types import { IUser } from "@plane/types"; -import { renderEmoji } from "helpers/emoji.helper"; +import { ReactionSelector } from "./reaction-selector"; export type TIssueReaction = { workspaceSlug: string; diff --git a/web/components/issues/issue-detail/reactions/reaction-selector.tsx b/web/components/issues/issue-detail/reactions/reaction-selector.tsx index 0782e7e15..655fd9105 100644 --- a/web/components/issues/issue-detail/reactions/reaction-selector.tsx +++ b/web/components/issues/issue-detail/reactions/reaction-selector.tsx @@ -1,9 +1,9 @@ import { Fragment } from "react"; import { Popover, Transition } from "@headlessui/react"; // helper +import { SmilePlus } from "lucide-react"; import { renderEmoji } from "helpers/emoji.helper"; // icons -import { SmilePlus } from "lucide-react"; const reactionEmojis = ["128077", "128078", "128516", "128165", "128533", "129505", "9992", "128064"]; diff --git a/web/components/issues/issue-detail/relation-select.tsx b/web/components/issues/issue-detail/relation-select.tsx index 260377406..0fd0902c6 100644 --- a/web/components/issues/issue-detail/relation-select.tsx +++ b/web/components/issues/issue-detail/relation-select.tsx @@ -1,15 +1,15 @@ import React from "react"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; import { CircleDot, CopyPlus, Pencil, X, XCircle } from "lucide-react"; // hooks +import { RelatedIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; +import { ExistingIssuesListModal } from "components/core"; +import { cn } from "helpers/common.helper"; import { useIssueDetail, useIssues, useProject } from "hooks/store"; // components -import { ExistingIssuesListModal } from "components/core"; // ui -import { RelatedIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // helpers -import { cn } from "helpers/common.helper"; // types import { TIssueRelationTypes, ISearchIssueResponse } from "@plane/types"; @@ -99,7 +99,7 @@ export const IssueRelationSelect: React.FC = observer((pro
    -
    Properties
    +
    Properties
    {/* TODO: render properties using a common component */} -
    -
    -
    +
    +
    +
    State
    @@ -195,7 +199,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { projectId={projectId?.toString() ?? ""} disabled={!is_editable} buttonVariant="transparent-with-text" - className="w-3/5 flex-grow group" + className="group w-3/5 flex-grow" buttonContainerClassName="w-full text-left" buttonClassName="text-sm" dropdownArrow @@ -203,8 +207,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { />
    -
    -
    +
    +
    Assignees
    @@ -216,7 +220,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { placeholder="Add assignees" multiple buttonVariant={issue?.assignee_ids?.length > 1 ? "transparent-without-text" : "transparent-with-text"} - className="w-3/5 flex-grow group" + className="group w-3/5 flex-grow" buttonContainerClassName="w-full text-left" buttonClassName={`text-sm justify-between ${ issue?.assignee_ids.length > 0 ? "" : "text-custom-text-400" @@ -227,8 +231,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { />
    -
    -
    +
    +
    Priority
    @@ -243,8 +247,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { />
    -
    -
    +
    +
    Start date
    @@ -259,7 +263,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { maxDate={maxDate ?? undefined} disabled={!is_editable} buttonVariant="transparent-with-text" - className="w-3/5 flex-grow group" + className="group w-3/5 flex-grow" buttonContainerClassName="w-full text-left" buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`} hideIcon @@ -269,8 +273,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { />
    -
    -
    +
    +
    Due date
    @@ -285,7 +289,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { minDate={minDate ?? undefined} disabled={!is_editable} buttonVariant="transparent-with-text" - className="w-3/5 flex-grow group" + className="group w-3/5 flex-grow" buttonContainerClassName="w-full text-left" buttonClassName={cn("text-sm", { "text-custom-text-400": !issue.target_date, @@ -299,8 +303,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => {
    {areEstimatesEnabledForCurrentProject && ( -
    -
    +
    +
    Estimate
    @@ -310,7 +314,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { projectId={projectId} disabled={!is_editable} buttonVariant="transparent-with-text" - className="w-3/5 flex-grow group" + className="group w-3/5 flex-grow" buttonContainerClassName="w-full text-left" buttonClassName={`text-sm ${issue?.estimate_point !== null ? "" : "text-custom-text-400"}`} placeholder="None" @@ -322,8 +326,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { )} {projectDetails?.module_view && ( -
    -
    +
    +
    Module
    @@ -339,8 +343,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { )} {projectDetails?.cycle_view && ( -
    -
    +
    +
    Cycle
    @@ -355,13 +359,13 @@ export const IssueDetailsSidebar: React.FC = observer((props) => {
    )} -
    -
    +
    +
    Parent
    = observer((props) => { />
    -
    -
    +
    +
    Relates to
    = observer((props) => { />
    -
    -
    +
    +
    Blocking
    = observer((props) => { />
    -
    -
    +
    +
    Blocked by
    = observer((props) => { />
    -
    -
    +
    +
    Duplicate of
    = observer((props) => { />
    -
    -
    +
    +
    Labels
    -
    +
    { const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); diff --git a/web/components/issues/issue-layouts/calendar/roots/module-root.tsx b/web/components/issues/issue-layouts/calendar/roots/module-root.tsx index cb474d25e..fd0153fe8 100644 --- a/web/components/issues/issue-layouts/calendar/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/module-root.tsx @@ -1,15 +1,15 @@ -import { useRouter } from "next/router"; +import { useMemo } from "react"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hoks +import { ModuleIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components -import { ModuleIssueQuickActions } from "components/issues"; // types import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; import { BaseCalendarRoot } from "../base-calendar-root"; -import { EIssuesStoreType } from "constants/issue"; -import { useMemo } from "react"; export const ModuleCalendarLayout: React.FC = observer(() => { const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); diff --git a/web/components/issues/issue-layouts/calendar/roots/project-root.tsx b/web/components/issues/issue-layouts/calendar/roots/project-root.tsx index d42a8c5d2..f8933a227 100644 --- a/web/components/issues/issue-layouts/calendar/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/project-root.tsx @@ -1,14 +1,14 @@ +import { useMemo } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks +import { ProjectIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components -import { ProjectIssueQuickActions } from "components/issues"; -import { BaseCalendarRoot } from "../base-calendar-root"; -import { EIssueActions } from "../../types"; import { TIssue } from "@plane/types"; -import { EIssuesStoreType } from "constants/issue"; -import { useMemo } from "react"; +import { EIssueActions } from "../../types"; +import { BaseCalendarRoot } from "../base-calendar-root"; export const CalendarLayout: React.FC = observer(() => { const router = useRouter(); diff --git a/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx b/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx index 0110aea2b..b50efd6c7 100644 --- a/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx @@ -1,15 +1,15 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { ProjectIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components -import { ProjectIssueQuickActions } from "components/issues"; -import { BaseCalendarRoot } from "../base-calendar-root"; // types import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; +import { BaseCalendarRoot } from "../base-calendar-root"; // constants -import { EIssuesStoreType } from "constants/issue"; export interface IViewCalendarLayout { issueActions: { diff --git a/web/components/issues/issue-layouts/calendar/week-days.tsx b/web/components/issues/issue-layouts/calendar/week-days.tsx index 5a640a566..2ce742fe8 100644 --- a/web/components/issues/issue-layouts/calendar/week-days.tsx +++ b/web/components/issues/issue-layouts/calendar/week-days.tsx @@ -4,12 +4,12 @@ import { CalendarDayTile } from "components/issues"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // types -import { ICalendarDate, ICalendarWeek } from "./types"; -import { TGroupedIssues, TIssue, TIssueMap } from "@plane/types"; import { ICycleIssuesFilter } from "store/issue/cycle"; import { IModuleIssuesFilter } from "store/issue/module"; import { IProjectIssuesFilter } from "store/issue/project"; import { IProjectViewIssuesFilter } from "store/issue/project-views"; +import { TGroupedIssues, TIssue, TIssueMap } from "@plane/types"; +import { ICalendarDate, ICalendarWeek } from "./types"; type Props = { issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; diff --git a/web/components/issues/issue-layouts/empty-states/archived-issues.tsx b/web/components/issues/issue-layouts/empty-states/archived-issues.tsx index 0c8fb377a..96887ed60 100644 --- a/web/components/issues/issue-layouts/empty-states/archived-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/archived-issues.tsx @@ -1,15 +1,15 @@ +import size from "lodash/size"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import size from "lodash/size"; import { useTheme } from "next-themes"; // hooks +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; import { useIssues, useUser } from "hooks/store"; // components -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // constants -import { EUserProjectRoles } from "constants/project"; -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; -import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state"; // types import { IIssueFilterOptions } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/empty-states/cycle.tsx b/web/components/issues/issue-layouts/empty-states/cycle.tsx index b23b1998e..7b86c16a5 100644 --- a/web/components/issues/issue-layouts/empty-states/cycle.tsx +++ b/web/components/issues/issue-layouts/empty-states/cycle.tsx @@ -1,20 +1,21 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; -import { PlusIcon } from "lucide-react"; import { useTheme } from "next-themes"; +import { PlusIcon } from "lucide-react"; // hooks -import { useApplication, useEventTracker, useIssueDetail, useIssues, useUser } from "hooks/store"; -// ui import { TOAST_TYPE, setToast } from "@plane/ui"; -// components import { ExistingIssuesListModal } from "components/core"; +// ui +// components import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { CYCLE_EMPTY_STATE_DETAILS, EMPTY_FILTER_STATE_DETAILS } from "constants/empty-state"; +import { EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; +// components // types import { ISearchIssueResponse, TIssueLayouts } from "@plane/types"; // constants -import { EUserProjectRoles } from "constants/project"; -import { EIssuesStoreType } from "constants/issue"; -import { CYCLE_EMPTY_STATE_DETAILS, EMPTY_FILTER_STATE_DETAILS } from "constants/empty-state"; type Props = { workspaceSlug: string | undefined; @@ -44,7 +45,6 @@ export const CycleEmptyState: React.FC = observer((props) => { const { resolvedTheme } = useTheme(); // store hooks const { issues } = useIssues(EIssuesStoreType.CYCLE); - const { updateIssue, fetchIssue } = useIssueDetail(); const { commandPalette: { toggleCreateIssueModal }, } = useApplication(); diff --git a/web/components/issues/issue-layouts/empty-states/draft-issues.tsx b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx index c496cc5fe..77b1123b6 100644 --- a/web/components/issues/issue-layouts/empty-states/draft-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx @@ -1,15 +1,15 @@ +import size from "lodash/size"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import size from "lodash/size"; import { useTheme } from "next-themes"; // hooks +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; import { useIssues, useUser } from "hooks/store"; // components -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // constants -import { EUserProjectRoles } from "constants/project"; -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; -import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state"; // types import { IIssueFilterOptions } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/empty-states/global-view.tsx b/web/components/issues/issue-layouts/empty-states/global-view.tsx index bf898aec4..b24c4d5d6 100644 --- a/web/components/issues/issue-layouts/empty-states/global-view.tsx +++ b/web/components/issues/issue-layouts/empty-states/global-view.tsx @@ -1,9 +1,9 @@ import { observer } from "mobx-react-lite"; import { Plus, PlusIcon } from "lucide-react"; // hooks +import { EmptyState } from "components/common"; import { useApplication, useEventTracker, useProject } from "hooks/store"; // components -import { EmptyState } from "components/common"; // assets import emptyIssue from "public/empty-state/issue.svg"; import emptyProject from "public/empty-state/project.svg"; diff --git a/web/components/issues/issue-layouts/empty-states/module.tsx b/web/components/issues/issue-layouts/empty-states/module.tsx index 7a5c6f57f..c52d17af5 100644 --- a/web/components/issues/issue-layouts/empty-states/module.tsx +++ b/web/components/issues/issue-layouts/empty-states/module.tsx @@ -1,20 +1,20 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; -import { PlusIcon } from "lucide-react"; import { useTheme } from "next-themes"; +import { PlusIcon } from "lucide-react"; // hooks -import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; -// ui import { TOAST_TYPE, setToast } from "@plane/ui"; -// components import { ExistingIssuesListModal } from "components/core"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EMPTY_FILTER_STATE_DETAILS, MODULE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; +// ui +// components // types import { ISearchIssueResponse, TIssueLayouts } from "@plane/types"; // constants -import { EUserProjectRoles } from "constants/project"; -import { EIssuesStoreType } from "constants/issue"; -import { EMPTY_FILTER_STATE_DETAILS, MODULE_EMPTY_STATE_DETAILS } from "constants/empty-state"; type Props = { workspaceSlug: string | undefined; diff --git a/web/components/issues/issue-layouts/empty-states/project-issues.tsx b/web/components/issues/issue-layouts/empty-states/project-issues.tsx index c7185934c..e44dd5626 100644 --- a/web/components/issues/issue-layouts/empty-states/project-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/project-issues.tsx @@ -1,15 +1,15 @@ +import size from "lodash/size"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import size from "lodash/size"; import { useTheme } from "next-themes"; // hooks +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; // components -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // constants -import { EUserProjectRoles } from "constants/project"; -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; -import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state"; // types import { IIssueFilterOptions } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/empty-states/project-view.tsx b/web/components/issues/issue-layouts/empty-states/project-view.tsx index 2da9a826f..fd98011fa 100644 --- a/web/components/issues/issue-layouts/empty-states/project-view.tsx +++ b/web/components/issues/issue-layouts/empty-states/project-view.tsx @@ -1,12 +1,12 @@ import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; // hooks +import { EmptyState } from "components/common"; +import { EIssuesStoreType } from "constants/issue"; import { useApplication, useEventTracker } from "hooks/store"; // components -import { EmptyState } from "components/common"; // assets import emptyIssue from "public/empty-state/issue.svg"; -import { EIssuesStoreType } from "constants/issue"; export const ProjectViewEmptyState: React.FC = observer(() => { // store hooks diff --git a/web/components/issues/issue-layouts/filters/applied-filters/cycle.tsx b/web/components/issues/issue-layouts/filters/applied-filters/cycle.tsx index 6299bebd7..76f36e815 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/cycle.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/cycle.tsx @@ -1,9 +1,9 @@ import { observer } from "mobx-react-lite"; import { X } from "lucide-react"; // hooks +import { CycleGroupIcon } from "@plane/ui"; import { useCycle } from "hooks/store"; // ui -import { CycleGroupIcon } from "@plane/ui"; // types import { TCycleGroups } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/date.tsx b/web/components/issues/issue-layouts/filters/applied-filters/date.tsx index 891fd6ddd..fdaed4b9b 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/date.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/date.tsx @@ -2,10 +2,10 @@ import { observer } from "mobx-react-lite"; // icons import { X } from "lucide-react"; // helpers +import { DATE_FILTER_OPTIONS } from "constants/filters"; import { renderFormattedDate } from "helpers/date-time.helper"; import { capitalizeFirstLetter } from "helpers/string.helper"; // constants -import { DATE_FILTER_OPTIONS } from "constants/filters"; type Props = { handleRemove: (val: string) => void; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx b/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx index 03b0c5138..10ad265f3 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx @@ -1,9 +1,6 @@ import { observer } from "mobx-react-lite"; import { X } from "lucide-react"; -import { useRouter } from "next/router"; // hooks -import { useApplication, useUser } from "hooks/store"; -// components import { AppliedCycleFilters, AppliedDateFilters, @@ -15,12 +12,14 @@ import { AppliedStateFilters, AppliedStateGroupFilters, } from "components/issues"; -// helpers +import { EUserProjectRoles } from "constants/project"; import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; +import { useApplication, useUser } from "hooks/store"; +// components +// helpers // types import { IIssueFilterOptions, IIssueLabel, IState } from "@plane/types"; // constants -import { EUserProjectRoles } from "constants/project"; type Props = { appliedFilters: IIssueFilterOptions; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/module.tsx b/web/components/issues/issue-layouts/filters/applied-filters/module.tsx index 790383f61..e34af8434 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/module.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/module.tsx @@ -1,9 +1,9 @@ import { observer } from "mobx-react-lite"; import { X } from "lucide-react"; // hooks +import { DiceIcon } from "@plane/ui"; import { useModule } from "hooks/store"; // ui -import { DiceIcon } from "@plane/ui"; type Props = { handleRemove: (val: string) => void; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/priority.tsx b/web/components/issues/issue-layouts/filters/applied-filters/priority.tsx index be3240b55..aad394d8a 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/priority.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/priority.tsx @@ -1,8 +1,8 @@ import { observer } from "mobx-react-lite"; // icons -import { PriorityIcon } from "@plane/ui"; import { X } from "lucide-react"; +import { PriorityIcon } from "@plane/ui"; // types import { TIssuePriorities } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/project.tsx b/web/components/issues/issue-layouts/filters/applied-filters/project.tsx index 4c5affe8d..24e8fd338 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/project.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/project.tsx @@ -1,9 +1,9 @@ import { observer } from "mobx-react-lite"; import { X } from "lucide-react"; // hooks +import { renderEmoji } from "helpers/emoji.helper"; import { useProject } from "hooks/store"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; type Props = { handleRemove: (val: string) => void; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx index 227dc025b..35651d870 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx @@ -1,12 +1,12 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { AppliedFiltersList, SaveFilterView } from "components/issues"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { useIssues, useLabel, useProjectState } from "hooks/store"; // components -import { AppliedFiltersList, SaveFilterView } from "components/issues"; // types import { IIssueFilterOptions } from "@plane/types"; -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => { // router diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx index daa194c9d..57e28240b 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx @@ -1,12 +1,12 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { AppliedFiltersList, SaveFilterView } from "components/issues"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { useIssues, useLabel, useProjectState } from "hooks/store"; // components -import { AppliedFiltersList, SaveFilterView } from "components/issues"; // types import { IIssueFilterOptions } from "@plane/types"; -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; export const CycleAppliedFiltersRoot: React.FC = observer(() => { // router diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx index e9024afeb..a075d59d2 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx @@ -1,12 +1,12 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { AppliedFiltersList, SaveFilterView } from "components/issues"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { useIssues, useLabel, useProjectState } from "hooks/store"; // components -import { AppliedFiltersList, SaveFilterView } from "components/issues"; // types import { IIssueFilterOptions } from "@plane/types"; -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => { // router diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx index c03e86504..a431652f1 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx @@ -1,18 +1,18 @@ -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import isEqual from "lodash/isEqual"; +import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks -import { useEventTracker, useGlobalView, useIssues, useLabel, useUser } from "hooks/store"; //ui import { Button } from "@plane/ui"; // components import { AppliedFiltersList } from "components/issues"; // types -import { IIssueFilterOptions, TStaticViewTypes } from "@plane/types"; +import { GLOBAL_VIEW_UPDATED } from "constants/event-tracker"; import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "constants/workspace"; // constants -import { GLOBAL_VIEW_UPDATED } from "constants/event-tracker"; +import { useEventTracker, useGlobalView, useIssues, useLabel, useUser } from "hooks/store"; +import { IIssueFilterOptions, TStaticViewTypes } from "@plane/types"; type Props = { globalViewId: string; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx index 055c32d20..d2c9ba7ed 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx @@ -1,12 +1,12 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { AppliedFiltersList, SaveFilterView } from "components/issues"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { useIssues, useLabel, useProjectState } from "hooks/store"; // components -import { AppliedFiltersList, SaveFilterView } from "components/issues"; // types import { IIssueFilterOptions } from "@plane/types"; -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; export const ModuleAppliedFiltersRoot: React.FC = observer(() => { // router diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx index 7a6c39336..91eeef423 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx @@ -1,13 +1,13 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks -import { useIssues, useLabel } from "hooks/store"; // components import { AppliedFiltersList } from "components/issues"; // types -import { IIssueFilterOptions } from "@plane/types"; import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { useIssues, useLabel } from "hooks/store"; import { useWorkspaceIssueProperties } from "hooks/use-workspace-issue-properties"; +import { IIssueFilterOptions } from "@plane/types"; export const ProfileIssuesAppliedFiltersRoot: React.FC = observer(() => { // router diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx index 68b5e6727..c0b67043a 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx @@ -1,13 +1,13 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks -import { useLabel, useProjectState, useUser } from "hooks/store"; -import { useIssues } from "hooks/store/use-issues"; // components import { AppliedFiltersList, SaveFilterView } from "components/issues"; // constants import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; +import { useLabel, useProjectState, useUser } from "hooks/store"; +import { useIssues } from "hooks/store/use-issues"; // types import { IIssueFilterOptions } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx index 0768064ec..278e19d65 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx @@ -1,15 +1,15 @@ -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import isEqual from "lodash/isEqual"; +import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { Button } from "@plane/ui"; +import { AppliedFiltersList } from "components/issues"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { useIssues, useLabel, useProjectState, useProjectView } from "hooks/store"; // components -import { AppliedFiltersList } from "components/issues"; // ui -import { Button } from "@plane/ui"; // types import { IIssueFilterOptions } from "@plane/types"; -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { // router diff --git a/web/components/issues/issue-layouts/filters/applied-filters/state-group.tsx b/web/components/issues/issue-layouts/filters/applied-filters/state-group.tsx index 620a8f781..b4a6baa53 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/state-group.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/state-group.tsx @@ -1,8 +1,8 @@ import { observer } from "mobx-react-lite"; // icons -import { StateGroupIcon } from "@plane/ui"; import { X } from "lucide-react"; +import { StateGroupIcon } from "@plane/ui"; import { TStateGroups } from "@plane/types"; type Props = { diff --git a/web/components/issues/issue-layouts/filters/applied-filters/state.tsx b/web/components/issues/issue-layouts/filters/applied-filters/state.tsx index 59a873162..fc216afad 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/state.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/state.tsx @@ -1,8 +1,8 @@ import { observer } from "mobx-react-lite"; // icons -import { StateGroupIcon } from "@plane/ui"; import { X } from "lucide-react"; +import { StateGroupIcon } from "@plane/ui"; // types import { IState } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx index b8988580a..02d1b2f04 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx @@ -11,8 +11,8 @@ import { FilterSubGroupBy, } from "components/issues"; // types -import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssueGroupByOptions } from "@plane/types"; import { ILayoutDisplayFiltersOptions } from "constants/issue"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssueGroupByOptions } from "@plane/types"; type Props = { displayFilters: IIssueDisplayFilterOptions; @@ -37,7 +37,7 @@ export const DisplayFiltersSelection: React.FC = observer((props) => { Object.keys(layoutDisplayFiltersOptions?.display_filters ?? {}).includes(displayFilter); return ( -
    +
    {/* display properties */} {layoutDisplayFiltersOptions?.display_properties && (
    diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx index f97140185..871bf8ff5 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx @@ -2,11 +2,11 @@ import React from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // components +import { ISSUE_DISPLAY_PROPERTIES } from "constants/issue"; +import { IIssueDisplayProperties } from "@plane/types"; import { FilterHeader } from "../helpers/filter-header"; // types -import { IIssueDisplayProperties } from "@plane/types"; // constants -import { ISSUE_DISPLAY_PROPERTIES } from "constants/issue"; type Props = { displayProperties: IIssueDisplayProperties; diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx index 0feb1d891..6de3c940d 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx @@ -4,9 +4,9 @@ import { observer } from "mobx-react-lite"; // components import { FilterOption } from "components/issues"; // types +import { ISSUE_EXTRA_OPTIONS } from "constants/issue"; import { IIssueDisplayFilterOptions, TIssueExtraOptions } from "@plane/types"; // constants -import { ISSUE_EXTRA_OPTIONS } from "constants/issue"; type Props = { selectedExtraOptions: { diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx index a4478e834..10dfa8c7c 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx @@ -3,9 +3,9 @@ import { observer } from "mobx-react-lite"; // components import { FilterHeader, FilterOption } from "components/issues"; // types +import { ISSUE_GROUP_BY_OPTIONS } from "constants/issue"; import { IIssueDisplayFilterOptions, TIssueGroupByOptions } from "@plane/types"; // constants -import { ISSUE_GROUP_BY_OPTIONS } from "constants/issue"; type Props = { displayFilters: IIssueDisplayFilterOptions; diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/issue-type.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/issue-type.tsx index 59c83a200..9cdcf953b 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/issue-type.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/issue-type.tsx @@ -4,9 +4,9 @@ import { observer } from "mobx-react-lite"; // components import { FilterHeader, FilterOption } from "components/issues"; // types +import { ISSUE_FILTER_OPTIONS } from "constants/issue"; import { TIssueTypeFilters } from "@plane/types"; // constants -import { ISSUE_FILTER_OPTIONS } from "constants/issue"; type Props = { selectedIssueType: TIssueTypeFilters | undefined; diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/order-by.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/order-by.tsx index e417c650e..afcd0ba1b 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/order-by.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/order-by.tsx @@ -4,9 +4,9 @@ import { observer } from "mobx-react-lite"; // components import { FilterHeader, FilterOption } from "components/issues"; // types +import { ISSUE_ORDER_BY_OPTIONS } from "constants/issue"; import { TIssueOrderByOptions } from "@plane/types"; // constants -import { ISSUE_ORDER_BY_OPTIONS } from "constants/issue"; type Props = { selectedOrderBy: TIssueOrderByOptions | undefined; diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/sub-group-by.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/sub-group-by.tsx index 198148a84..98dcb7b95 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/sub-group-by.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/sub-group-by.tsx @@ -3,9 +3,9 @@ import { observer } from "mobx-react-lite"; // components import { FilterHeader, FilterOption } from "components/issues"; // types +import { ISSUE_GROUP_BY_OPTIONS } from "constants/issue"; import { IIssueDisplayFilterOptions, TIssueGroupByOptions } from "@plane/types"; // constants -import { ISSUE_GROUP_BY_OPTIONS } from "constants/issue"; type Props = { displayFilters: IIssueDisplayFilterOptions; diff --git a/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx b/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx index 168e31bc0..b26b688af 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx @@ -1,11 +1,11 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; // hooks +import { Avatar, Loader } from "@plane/ui"; +import { FilterHeader, FilterOption } from "components/issues"; import { useMember } from "hooks/store"; // components -import { FilterHeader, FilterOption } from "components/issues"; // ui -import { Avatar, Loader } from "@plane/ui"; type Props = { appliedFilters: string[] | null; @@ -24,8 +24,8 @@ export const FilterAssignees: React.FC = observer((props: Props) => { const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = memberIds?.filter((memberId) => - getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) + const filteredOptions = memberIds?.filter( + (memberId) => getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) ); const handleViewToggle = () => { diff --git a/web/components/issues/issue-layouts/filters/header/filters/created-by.tsx b/web/components/issues/issue-layouts/filters/header/filters/created-by.tsx index 7bde26ab9..45e3309a9 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/created-by.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/created-by.tsx @@ -1,11 +1,11 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; // hooks +import { Avatar, Loader } from "@plane/ui"; +import { FilterHeader, FilterOption } from "components/issues"; import { useMember } from "hooks/store"; // components -import { FilterHeader, FilterOption } from "components/issues"; // ui -import { Avatar, Loader } from "@plane/ui"; type Props = { appliedFilters: string[] | null; @@ -22,8 +22,8 @@ export const FilterCreatedBy: React.FC = observer((props: Props) => { // store hooks const { getUserDetails } = useMember(); - const filteredOptions = memberIds?.filter((memberId) => - getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) + const filteredOptions = memberIds?.filter( + (memberId) => getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) ); const appliedFiltersCount = appliedFilters?.length ?? 0; diff --git a/web/components/issues/issue-layouts/filters/header/filters/cycle.tsx b/web/components/issues/issue-layouts/filters/header/filters/cycle.tsx index 47b3b0506..396addde6 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/cycle.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/cycle.tsx @@ -1,11 +1,11 @@ import React, { useState } from "react"; -import { observer } from "mobx-react"; import sortBy from "lodash/sortBy"; +import { observer } from "mobx-react"; // components +import { Loader, CycleGroupIcon } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; import { useApplication, useCycle } from "hooks/store"; // ui -import { Loader, CycleGroupIcon } from "@plane/ui"; // types import { TCycleGroups } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx b/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx index ae7ded8b2..257aa1977 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx @@ -2,8 +2,6 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; import { Search, X } from "lucide-react"; // hooks -import { useApplication } from "hooks/store"; -// components import { FilterAssignees, FilterMentions, @@ -18,10 +16,12 @@ import { FilterCycle, FilterModule, } from "components/issues"; +import { ILayoutDisplayFiltersOptions } from "constants/issue"; +import { useApplication } from "hooks/store"; +// components // types import { IIssueFilterOptions, IIssueLabel, IState } from "@plane/types"; // constants -import { ILayoutDisplayFiltersOptions } from "constants/issue"; type Props = { filters: IIssueFilterOptions; diff --git a/web/components/issues/issue-layouts/filters/header/filters/labels.tsx b/web/components/issues/issue-layouts/filters/header/filters/labels.tsx index b226f42b3..42e955535 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/labels.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/labels.tsx @@ -1,9 +1,9 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; // components +import { Loader } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; // ui -import { Loader } from "@plane/ui"; // types import { IIssueLabel } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx b/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx index a6af9833a..4d2839b2c 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx @@ -1,11 +1,11 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; // hooks +import { Loader, Avatar } from "@plane/ui"; +import { FilterHeader, FilterOption } from "components/issues"; import { useMember } from "hooks/store"; // components -import { FilterHeader, FilterOption } from "components/issues"; // ui -import { Loader, Avatar } from "@plane/ui"; type Props = { appliedFilters: string[] | null; @@ -24,8 +24,8 @@ export const FilterMentions: React.FC = observer((props: Props) => { const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = memberIds?.filter((memberId) => - getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) + const filteredOptions = memberIds?.filter( + (memberId) => getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) ); const handleViewToggle = () => { diff --git a/web/components/issues/issue-layouts/filters/header/filters/module.tsx b/web/components/issues/issue-layouts/filters/header/filters/module.tsx index 49e00f84d..812cf939f 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/module.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/module.tsx @@ -1,11 +1,11 @@ import React, { useState } from "react"; -import { observer } from "mobx-react"; import sortBy from "lodash/sortBy"; +import { observer } from "mobx-react"; // components +import { Loader, DiceIcon } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; import { useApplication, useModule } from "hooks/store"; // ui -import { Loader, DiceIcon } from "@plane/ui"; type Props = { appliedFilters: string[] | null; diff --git a/web/components/issues/issue-layouts/filters/header/filters/project.tsx b/web/components/issues/issue-layouts/filters/header/filters/project.tsx index 61b7d50c1..3bdde6623 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/project.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/project.tsx @@ -1,13 +1,13 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; // components +import { Loader } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; // hooks +import { renderEmoji } from "helpers/emoji.helper"; import { useProject } from "hooks/store"; // ui -import { Loader } from "@plane/ui"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; type Props = { appliedFilters: string[] | null; diff --git a/web/components/issues/issue-layouts/filters/header/filters/start-date.tsx b/web/components/issues/issue-layouts/filters/header/filters/start-date.tsx index 2cb715158..87def7e29 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/start-date.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/start-date.tsx @@ -2,8 +2,8 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; // components -import { FilterHeader, FilterOption } from "components/issues"; import { DateFilterModal } from "components/core"; +import { FilterHeader, FilterOption } from "components/issues"; // constants import { DATE_FILTER_OPTIONS } from "constants/filters"; diff --git a/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx b/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx index ea9097146..06c1aae9f 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx @@ -2,9 +2,9 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; // components +import { StateGroupIcon } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; // icons -import { StateGroupIcon } from "@plane/ui"; import { STATE_GROUPS } from "constants/state"; // constants diff --git a/web/components/issues/issue-layouts/filters/header/filters/state.tsx b/web/components/issues/issue-layouts/filters/header/filters/state.tsx index c13a69b0a..5dde1d279 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/state.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/state.tsx @@ -1,9 +1,9 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; // components +import { Loader, StateGroupIcon } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; // ui -import { Loader, StateGroupIcon } from "@plane/ui"; // types import { IState } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/filters/header/filters/target-date.tsx b/web/components/issues/issue-layouts/filters/header/filters/target-date.tsx index b168af668..9e0ce18a7 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/target-date.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/target-date.tsx @@ -2,8 +2,8 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; // components -import { FilterHeader, FilterOption } from "components/issues"; import { DateFilterModal } from "components/core"; +import { FilterHeader, FilterOption } from "components/issues"; // constants import { DATE_FILTER_OPTIONS } from "constants/filters"; diff --git a/web/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx b/web/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx index 33b86ada1..0d00c3675 100644 --- a/web/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx +++ b/web/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx @@ -1,11 +1,11 @@ import React, { Fragment, useState } from "react"; +import { Placement } from "@popperjs/core"; import { usePopper } from "react-popper"; import { Popover, Transition } from "@headlessui/react"; -import { Placement } from "@popperjs/core"; // ui +import { ChevronUp } from "lucide-react"; import { Button } from "@plane/ui"; // icons -import { ChevronUp } from "lucide-react"; type Props = { children: React.ReactNode; @@ -34,22 +34,26 @@ export const FiltersDropdown: React.FC = (props) => { return ( <> - {menuButton ? : } + {menuButton ? ( + + ) : ( + + )} { // router diff --git a/web/components/issues/issue-layouts/gantt/module-root.tsx b/web/components/issues/issue-layouts/gantt/module-root.tsx index c7c8e8b03..3311b6c6a 100644 --- a/web/components/issues/issue-layouts/gantt/module-root.tsx +++ b/web/components/issues/issue-layouts/gantt/module-root.tsx @@ -1,12 +1,12 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks +import { EIssuesStoreType } from "constants/issue"; import { useIssues, useModule } from "hooks/store"; // components -import { BaseGanttRoot } from "./base-gantt-root"; -import { EIssuesStoreType } from "constants/issue"; -import { EIssueActions } from "../types"; import { TIssue } from "@plane/types"; +import { EIssueActions } from "../types"; +import { BaseGanttRoot } from "./base-gantt-root"; export const ModuleGanttLayout: React.FC = observer(() => { // router diff --git a/web/components/issues/issue-layouts/gantt/project-root.tsx b/web/components/issues/issue-layouts/gantt/project-root.tsx index 18fd3ecef..1f9e560d3 100644 --- a/web/components/issues/issue-layouts/gantt/project-root.tsx +++ b/web/components/issues/issue-layouts/gantt/project-root.tsx @@ -1,13 +1,13 @@ import React from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components -import { BaseGanttRoot } from "./base-gantt-root"; -import { EIssuesStoreType } from "constants/issue"; -import { EIssueActions } from "../types"; import { TIssue } from "@plane/types"; +import { EIssueActions } from "../types"; +import { BaseGanttRoot } from "./base-gantt-root"; export const GanttLayout: React.FC = observer(() => { // router diff --git a/web/components/issues/issue-layouts/gantt/project-view-root.tsx b/web/components/issues/issue-layouts/gantt/project-view-root.tsx index 1ed02c2c9..cda2a1e53 100644 --- a/web/components/issues/issue-layouts/gantt/project-view-root.tsx +++ b/web/components/issues/issue-layouts/gantt/project-view-root.tsx @@ -1,14 +1,14 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components +import { TIssue } from "@plane/types"; +import { EIssueActions } from "../types"; import { BaseGanttRoot } from "./base-gantt-root"; // constants -import { EIssuesStoreType } from "constants/issue"; // types -import { EIssueActions } from "../types"; -import { TIssue } from "@plane/types"; export interface IViewGanttLayout { issueActions: { diff --git a/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx index 10b73ab35..94a6243e5 100644 --- a/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx @@ -1,22 +1,21 @@ import { useEffect, useState, useRef, FC } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { useForm } from "react-hook-form"; -import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; // hooks +import { setPromiseToast } from "@plane/ui"; +import { cn } from "helpers/common.helper"; +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { createIssuePayload } from "helpers/issue.helper"; import { useEventTracker, useProject } from "hooks/store"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // helpers -import { renderFormattedPayloadDate } from "helpers/date-time.helper"; -import { createIssuePayload } from "helpers/issue.helper"; -import { cn } from "helpers/common.helper"; // ui -import { setPromiseToast } from "@plane/ui"; // types import { IProject, TIssue } from "@plane/types"; // constants -import { ISSUE_CREATED } from "constants/event-tracker"; interface IInputProps { formKey: string; diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx index 3951e7032..775382f59 100644 --- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -1,30 +1,29 @@ import { FC, useCallback, useRef, useState } from "react"; import { DragDropContext, DragStart, DraggableLocation, DropResult, Droppable } from "@hello-pangea/dnd"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { Spinner, TOAST_TYPE, setToast } from "@plane/ui"; +import { DeleteIssueModal } from "components/issues"; +import { ISSUE_DELETED } from "constants/event-tracker"; +import { EIssueFilterType, TCreateModalStoreTypes } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; import { useEventTracker, useUser } from "hooks/store"; // ui -import { Spinner, TOAST_TYPE, setToast } from "@plane/ui"; // types -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../types"; -import { IQuickActionProps } from "../list/list-view-types"; +import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle"; +import { IDraftIssues, IDraftIssuesFilter } from "store/issue/draft"; +import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module"; +import { IProfileIssues, IProfileIssuesFilter } from "store/issue/profile"; import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project"; +import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views"; +import { TIssue } from "@plane/types"; +import { IQuickActionProps } from "../list/list-view-types"; +import { EIssueActions } from "../types"; //components import { KanBan } from "./default"; import { KanBanSwimLanes } from "./swimlanes"; -import { DeleteIssueModal } from "components/issues"; -import { EUserProjectRoles } from "constants/project"; -import { useIssues } from "hooks/store/use-issues"; import { handleDragDrop } from "./utils"; -import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle"; -import { IDraftIssues, IDraftIssuesFilter } from "store/issue/draft"; -import { IProfileIssues, IProfileIssuesFilter } from "store/issue/profile"; -import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module"; -import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views"; -import { EIssueFilterType, TCreateModalStoreTypes } from "constants/issue"; -import { ISSUE_DELETED } from "constants/event-tracker"; export interface IBaseKanBanLayout { issues: IProjectIssues | ICycleIssues | IDraftIssues | IModuleIssues | IProjectViewIssues | IProfileIssues; @@ -227,15 +226,15 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas const handleKanbanFilters = (toggle: "group_by" | "sub_group_by", value: string) => { if (workspaceSlug && projectId) { - let _kanbanFilters = issuesFilter?.issueFilters?.kanbanFilters?.[toggle] || []; - if (_kanbanFilters.includes(value)) _kanbanFilters = _kanbanFilters.filter((_value) => _value != value); - else _kanbanFilters.push(value); + let kanbanFilters = issuesFilter?.issueFilters?.kanbanFilters?.[toggle] || []; + if (kanbanFilters.includes(value)) kanbanFilters = kanbanFilters.filter((_value) => _value != value); + else kanbanFilters.push(value); issuesFilter.updateFilters( workspaceSlug.toString(), projectId.toString(), EIssueFilterType.KANBAN_FILTERS, { - [toggle]: _kanbanFilters, + [toggle]: kanbanFilters, }, viewId ); @@ -260,7 +259,7 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas )}
    @@ -288,7 +287,7 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas
    -
    +
    = memo((props) => { classNames="space-y-2 px-3 py-2" root={scrollableContainerRef} defaultHeight="100px" - horizonatlOffset={50} + horizontalOffset={50} alwaysRender={snapshot.isDragging} pauseHeightUpdateWhileRendering={isDragStarted} changingReference={issueIds} diff --git a/web/components/issues/issue-layouts/kanban/blocks-list.tsx b/web/components/issues/issue-layouts/kanban/blocks-list.tsx index 3746111e5..ff1c92873 100644 --- a/web/components/issues/issue-layouts/kanban/blocks-list.tsx +++ b/web/components/issues/issue-layouts/kanban/blocks-list.tsx @@ -1,9 +1,9 @@ import { MutableRefObject, memo } from "react"; //types +import { KanbanIssueBlock } from "components/issues"; import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types"; import { EIssueActions } from "../types"; // components -import { KanbanIssueBlock } from "components/issues"; interface IssueBlocksListProps { sub_group_id: string; diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx index 3cbab589f..ece578058 100644 --- a/web/components/issues/issue-layouts/kanban/default.tsx +++ b/web/components/issues/issue-layouts/kanban/default.tsx @@ -1,4 +1,7 @@ +import { MutableRefObject } from "react"; import { observer } from "mobx-react-lite"; +// constants +import { TCreateModalStoreTypes } from "constants/issue"; // hooks import { useCycle, @@ -10,9 +13,6 @@ import { useProject, useProjectState, } from "hooks/store"; -// components -import { HeaderGroupByCard } from "./headers/group-by-card"; -import { KanbanGroup } from "./kanban-group"; // types import { GroupByColumnTypes, @@ -25,11 +25,12 @@ import { TUnGroupedIssues, TIssueKanbanFilters, } from "@plane/types"; -// constants +// parent components import { EIssueActions } from "../types"; import { getGroupByColumns } from "../utils"; -import { TCreateModalStoreTypes } from "constants/issue"; -import { MutableRefObject } from "react"; +// components +import { HeaderGroupByCard } from "./headers/group-by-card"; +import { KanbanGroup } from "./kanban-group"; export interface IGroupByKanBan { issuesMap: IIssueMap; @@ -89,11 +90,19 @@ const GroupByKanBan: React.FC = observer((props) => { const project = useProject(); const label = useLabel(); const cycle = useCycle(); - const _module = useModule(); + const moduleInfo = useModule(); const projectState = useProjectState(); const { peekIssue } = useIssueDetail(); - const list = getGroupByColumns(group_by as GroupByColumnTypes, project, cycle, _module, label, projectState, member); + const list = getGroupByColumns( + group_by as GroupByColumnTypes, + project, + cycle, + moduleInfo, + label, + projectState, + member + ); if (!list) return null; @@ -114,16 +123,19 @@ const GroupByKanBan: React.FC = observer((props) => { const isGroupByCreatedBy = group_by === "created_by"; return ( -
    +
    {groupList && groupList.length > 0 && groupList.map((_list: IGroupByColumn) => { const groupByVisibilityToggle = visibilityGroupBy(_list); return ( -
    +
    {sub_group_by === null && ( -
    +
    = observer((props) => {
    diff --git a/web/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx b/web/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx index ea9464780..b0859a70d 100644 --- a/web/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx @@ -1,7 +1,7 @@ import React from "react"; +import { observer } from "mobx-react-lite"; import { Circle, ChevronDown, ChevronUp } from "lucide-react"; // mobx -import { observer } from "mobx-react-lite"; import { TIssueKanbanFilters } from "@plane/types"; interface IHeaderSubGroupByCard { diff --git a/web/components/issues/issue-layouts/kanban/kanban-group.tsx b/web/components/issues/issue-layouts/kanban/kanban-group.tsx index a05fb1791..9d7053216 100644 --- a/web/components/issues/issue-layouts/kanban/kanban-group.tsx +++ b/web/components/issues/issue-layouts/kanban/kanban-group.tsx @@ -3,7 +3,6 @@ import { Droppable } from "@hello-pangea/dnd"; // hooks import { useProjectState } from "hooks/store"; //components -import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from "."; //types import { TGroupedIssues, @@ -14,6 +13,7 @@ import { TUnGroupedIssues, } from "@plane/types"; import { EIssueActions } from "../types"; +import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from "."; interface IKanbanGroup { groupId: string; diff --git a/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx index 20f0cd8e0..71a0e661c 100644 --- a/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx @@ -1,20 +1,20 @@ import { useEffect, useState, useRef } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { useForm } from "react-hook-form"; -import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; // hooks +import { setPromiseToast } from "@plane/ui"; +import { ISSUE_CREATED } from "constants/event-tracker"; +import { createIssuePayload } from "helpers/issue.helper"; import { useEventTracker, useProject } from "hooks/store"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // helpers -import { createIssuePayload } from "helpers/issue.helper"; // ui -import { setPromiseToast } from "@plane/ui"; // types import { TIssue } from "@plane/types"; // constants -import { ISSUE_CREATED } from "constants/event-tracker"; const Inputs = (props: any) => { const { register, setFocus, projectDetail } = props; @@ -139,7 +139,7 @@ export const KanBanQuickAddIssueForm: React.FC = obser return ( <> {isOpen ? ( -
    +
    { issueActions={issueActions} issues={issues} issuesFilter={issuesFilter} - showLoader={true} + showLoader QuickActions={CycleIssueQuickActions} viewId={cycleId?.toString() ?? ""} storeType={EIssuesStoreType.CYCLE} diff --git a/web/components/issues/issue-layouts/kanban/roots/draft-issue-root.tsx b/web/components/issues/issue-layouts/kanban/roots/draft-issue-root.tsx index 9152dbfe5..501734134 100644 --- a/web/components/issues/issue-layouts/kanban/roots/draft-issue-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/draft-issue-root.tsx @@ -1,16 +1,16 @@ -import { useRouter } from "next/router"; +import { useMemo } from "react"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { ProjectIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components -import { ProjectIssueQuickActions } from "components/issues"; // types import { TIssue } from "@plane/types"; // constants import { EIssueActions } from "../../types"; import { BaseKanBanRoot } from "../base-kanban-root"; -import { EIssuesStoreType } from "constants/issue"; -import { useMemo } from "react"; export interface IKanBanLayout {} @@ -42,7 +42,7 @@ export const DraftKanBanLayout: React.FC = observer(() => { issueActions={issueActions} issuesFilter={issuesFilter} issues={issues} - showLoader={true} + showLoader QuickActions={ProjectIssueQuickActions} /> ); diff --git a/web/components/issues/issue-layouts/kanban/roots/module-root.tsx b/web/components/issues/issue-layouts/kanban/roots/module-root.tsx index 07ad7eb83..96cfaceda 100644 --- a/web/components/issues/issue-layouts/kanban/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/module-root.tsx @@ -1,16 +1,16 @@ import React, { useMemo } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hook +import { ModuleIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components -import { ModuleIssueQuickActions } from "components/issues"; // types import { TIssue } from "@plane/types"; // constants import { EIssueActions } from "../../types"; import { BaseKanBanRoot } from "../base-kanban-root"; -import { EIssuesStoreType } from "constants/issue"; export interface IModuleKanBanLayout {} @@ -52,7 +52,7 @@ export const ModuleKanBanLayout: React.FC = observer(() => { issueActions={issueActions} issues={issues} issuesFilter={issuesFilter} - showLoader={true} + showLoader QuickActions={ModuleIssueQuickActions} viewId={moduleId?.toString()} storeType={EIssuesStoreType.MODULE} diff --git a/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx index c6c041654..99d703a72 100644 --- a/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx @@ -1,17 +1,17 @@ -import { useRouter } from "next/router"; +import { useMemo } from "react"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { ProjectIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; import { useIssues, useUser } from "hooks/store"; // components -import { ProjectIssueQuickActions } from "components/issues"; // types import { TIssue } from "@plane/types"; // constants import { EIssueActions } from "../../types"; import { BaseKanBanRoot } from "../base-kanban-root"; -import { EUserProjectRoles } from "constants/project"; -import { EIssuesStoreType } from "constants/issue"; -import { useMemo } from "react"; export const ProfileIssuesKanBanLayout: React.FC = observer(() => { const router = useRouter(); @@ -55,7 +55,7 @@ export const ProfileIssuesKanBanLayout: React.FC = observer(() => { issueActions={issueActions} issuesFilter={issuesFilter} issues={issues} - showLoader={true} + showLoader QuickActions={ProjectIssueQuickActions} storeType={EIssuesStoreType.PROFILE} canEditPropertiesBasedOnProject={canEditPropertiesBasedOnProject} diff --git a/web/components/issues/issue-layouts/kanban/roots/project-root.tsx b/web/components/issues/issue-layouts/kanban/roots/project-root.tsx index efd86bc8e..432663a02 100644 --- a/web/components/issues/issue-layouts/kanban/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/project-root.tsx @@ -1,16 +1,16 @@ -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import { useMemo } from "react"; +import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // mobx store +import { ProjectIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store/use-issues"; // components -import { ProjectIssueQuickActions } from "components/issues"; -import { BaseKanBanRoot } from "../base-kanban-root"; // types import { TIssue } from "@plane/types"; // constants import { EIssueActions } from "../../types"; -import { EIssuesStoreType } from "constants/issue"; +import { BaseKanBanRoot } from "../base-kanban-root"; export interface IKanBanLayout {} @@ -46,7 +46,7 @@ export const KanBanLayout: React.FC = observer(() => { issueActions={issueActions} issues={issues} issuesFilter={issuesFilter} - showLoader={true} + showLoader QuickActions={ProjectIssueQuickActions} storeType={EIssuesStoreType.PROJECT} /> diff --git a/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx b/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx index 8dd33b728..77689e563 100644 --- a/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx @@ -2,15 +2,15 @@ import React from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // constant -import { EIssuesStoreType } from "constants/issue"; // types import { TIssue } from "@plane/types"; +import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; import { EIssueActions } from "../../types"; // components import { BaseKanBanRoot } from "../base-kanban-root"; -import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; export interface IViewKanBanLayout { issueActions: { @@ -34,7 +34,7 @@ export const ProjectViewKanBanLayout: React.FC = observer((pr issueActions={issueActions} issuesFilter={issuesFilter} issues={issues} - showLoader={true} + showLoader QuickActions={ProjectIssueQuickActions} storeType={EIssuesStoreType.PROJECT_VIEW} viewId={viewId?.toString()} diff --git a/web/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/components/issues/issue-layouts/kanban/swimlanes.tsx index d60e3b618..75cb830c6 100644 --- a/web/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/web/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -1,10 +1,8 @@ import { MutableRefObject } from "react"; import { observer } from "mobx-react-lite"; // components -import { KanBan } from "./default"; -import { HeaderSubGroupByCard } from "./headers/sub-group-by-card"; -import { HeaderGroupByCard } from "./headers/group-by-card"; -// types +import { TCreateModalStoreTypes } from "constants/issue"; +import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "hooks/store"; import { GroupByColumnTypes, IGroupByColumn, @@ -16,11 +14,13 @@ import { TUnGroupedIssues, TIssueKanbanFilters, } from "@plane/types"; -// constants import { EIssueActions } from "../types"; -import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "hooks/store"; import { getGroupByColumns } from "../utils"; -import { TCreateModalStoreTypes } from "constants/issue"; +import { KanBan } from "./default"; +import { HeaderGroupByCard } from "./headers/group-by-card"; +import { HeaderSubGroupByCard } from "./headers/sub-group-by-card"; +// types +// constants interface ISubGroupSwimlaneHeader { issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues; @@ -47,7 +47,7 @@ const SubGroupSwimlaneHeader: React.FC = ({ kanbanFilters, handleKanbanFilters, }) => ( -
    +
    {list && list.length > 0 && list.map((_list: IGroupByColumn) => ( @@ -129,7 +129,7 @@ const SubGroupSwimlane: React.FC = observer((props) => { {list && list.length > 0 && list.map((_list: any) => ( -
    +
    = observer((props) => { const project = useProject(); const label = useLabel(); const cycle = useCycle(); - const _module = useModule(); + const projectModule = useModule(); const projectState = useProjectState(); const groupByList = getGroupByColumns( group_by as GroupByColumnTypes, project, cycle, - _module, + projectModule, label, projectState, member @@ -243,7 +243,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { sub_group_by as GroupByColumnTypes, project, cycle, - _module, + projectModule, label, projectState, member diff --git a/web/components/issues/issue-layouts/list/base-list-root.tsx b/web/components/issues/issue-layouts/list/base-list-root.tsx index ffe9de661..8a3d87e40 100644 --- a/web/components/issues/issue-layouts/list/base-list-root.tsx +++ b/web/components/issues/issue-layouts/list/base-list-root.tsx @@ -1,23 +1,23 @@ -import { List } from "./default"; import { FC, useCallback } from "react"; import { observer } from "mobx-react-lite"; // types -import { TIssue } from "@plane/types"; -import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project"; +import { TCreateModalStoreTypes } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { useIssues, useUser } from "hooks/store"; +import { IArchivedIssuesFilter, IArchivedIssues } from "store/issue/archived"; import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle"; +import { IDraftIssuesFilter, IDraftIssues } from "store/issue/draft"; import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module"; import { IProfileIssues, IProfileIssuesFilter } from "store/issue/profile"; +import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project"; import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views"; -import { IDraftIssuesFilter, IDraftIssues } from "store/issue/draft"; -import { IArchivedIssuesFilter, IArchivedIssues } from "store/issue/archived"; +import { TIssue } from "@plane/types"; import { EIssueActions } from "../types"; // components +import { List } from "./default"; import { IQuickActionProps } from "./list-view-types"; // constants -import { EUserProjectRoles } from "constants/project"; -import { TCreateModalStoreTypes } from "constants/issue"; // hooks -import { useIssues, useUser } from "hooks/store"; interface IBaseListRoot { issuesFilter: diff --git a/web/components/issues/issue-layouts/list/block.tsx b/web/components/issues/issue-layouts/list/block.tsx index 90fee10cc..a2148634c 100644 --- a/web/components/issues/issue-layouts/list/block.tsx +++ b/web/components/issues/issue-layouts/list/block.tsx @@ -1,14 +1,14 @@ import { observer } from "mobx-react-lite"; // components -import { IssueProperties } from "../properties/all-properties"; // hooks -import { useApplication, useIssueDetail, useProject } from "hooks/store"; // ui import { Spinner, Tooltip, ControlLink } from "@plane/ui"; // helper import { cn } from "helpers/common.helper"; +import { useApplication, useIssueDetail, useProject } from "hooks/store"; // types import { TIssue, IIssueDisplayProperties, TIssueMap } from "@plane/types"; +import { IssueProperties } from "../properties/all-properties"; import { EIssueActions } from "../types"; interface IssueBlockProps { diff --git a/web/components/issues/issue-layouts/list/blocks-list.tsx b/web/components/issues/issue-layouts/list/blocks-list.tsx index d3c8d1406..23c364b67 100644 --- a/web/components/issues/issue-layouts/list/blocks-list.tsx +++ b/web/components/issues/issue-layouts/list/blocks-list.tsx @@ -1,10 +1,10 @@ import { FC, MutableRefObject } from "react"; // components +import RenderIfVisible from "components/core/render-if-visible-HOC"; import { IssueBlock } from "components/issues"; // types import { TGroupedIssues, TIssue, IIssueDisplayProperties, TIssueMap, TUnGroupedIssues } from "@plane/types"; import { EIssueActions } from "../types"; -import RenderIfVisible from "components/core/render-if-visible-HOC"; interface Props { issueIds: TGroupedIssues | TUnGroupedIssues | any; diff --git a/web/components/issues/issue-layouts/list/default.tsx b/web/components/issues/issue-layouts/list/default.tsx index c6f82c2be..db1bcb06a 100644 --- a/web/components/issues/issue-layouts/list/default.tsx +++ b/web/components/issues/issue-layouts/list/default.tsx @@ -1,9 +1,10 @@ import { useRef } from "react"; // components import { IssueBlocksList, ListQuickAddIssueForm } from "components/issues"; -import { HeaderGroupByCard } from "./headers/group-by-card"; // hooks +import { TCreateModalStoreTypes } from "constants/issue"; import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "hooks/store"; +// constants // types import { GroupByColumnTypes, @@ -15,9 +16,8 @@ import { IGroupByColumn, } from "@plane/types"; import { EIssueActions } from "../types"; -// constants -import { TCreateModalStoreTypes } from "constants/issue"; import { getGroupByColumns } from "../utils"; +import { HeaderGroupByCard } from "./headers/group-by-card"; export interface IGroupByList { issueIds: TGroupedIssues | TUnGroupedIssues | any; @@ -66,7 +66,7 @@ const GroupByList: React.FC = (props) => { const label = useLabel(); const projectState = useProjectState(); const cycle = useCycle(); - const _module = useModule(); + const projectModule = useModule(); const containerRef = useRef(null); @@ -74,7 +74,7 @@ const GroupByList: React.FC = (props) => { group_by as GroupByColumnTypes, project, cycle, - _module, + projectModule, label, projectState, member, @@ -119,7 +119,7 @@ const GroupByList: React.FC = (props) => { const isGroupByCreatedBy = group_by === "created_by"; return ( -
    +
    {groups && groups.length > 0 && groups.map( 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 404107af4..7edf89bf1 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,19 +1,19 @@ +import { useState } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // lucide icons import { CircleDashed, Plus } from "lucide-react"; // components -import { CreateUpdateIssueModal } from "components/issues"; -import { ExistingIssuesListModal } from "components/core"; -// ui import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; +import { ExistingIssuesListModal } from "components/core"; +import { CreateUpdateIssueModal } from "components/issues"; +// ui // mobx -import { observer } from "mobx-react-lite"; // hooks +import { TCreateModalStoreTypes } from "constants/issue"; import { useEventTracker } from "hooks/store"; // types import { TIssue, ISearchIssueResponse } from "@plane/types"; -import { useState } from "react"; -import { TCreateModalStoreTypes } from "constants/issue"; interface IHeaderGroupByCard { icon?: React.ReactNode; diff --git a/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx index 3c71293b4..7bae7ecff 100644 --- a/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx @@ -1,20 +1,20 @@ -import { FC, useEffect, useState, useRef, use } from "react"; +import { FC, useEffect, useState, useRef } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { useForm } from "react-hook-form"; import { PlusIcon } from "lucide-react"; -import { observer } from "mobx-react-lite"; // hooks +import { setPromiseToast } from "@plane/ui"; +import { ISSUE_CREATED } from "constants/event-tracker"; +import { createIssuePayload } from "helpers/issue.helper"; import { useEventTracker, useProject } from "hooks/store"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // ui -import { setPromiseToast } from "@plane/ui"; // types import { TIssue, IProject } from "@plane/types"; // helper -import { createIssuePayload } from "helpers/issue.helper"; // constants -import { ISSUE_CREATED } from "constants/event-tracker"; interface IInputProps { formKey: string; diff --git a/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx b/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx index 6e70d00d0..2f3807beb 100644 --- a/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx @@ -1,16 +1,16 @@ import { FC, useMemo } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { ArchivedIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components -import { ArchivedIssueQuickActions } from "components/issues"; // types import { TIssue } from "@plane/types"; // constants -import { BaseListRoot } from "../base-list-root"; import { EIssueActions } from "../../types"; -import { EIssuesStoreType } from "constants/issue"; +import { BaseListRoot } from "../base-list-root"; export const ArchivedIssueListLayout: FC = observer(() => { const router = useRouter(); diff --git a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx index 5c15ebe60..46ee7f32e 100644 --- a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx @@ -1,16 +1,16 @@ import React, { useCallback, useMemo } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { CycleIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; import { useCycle, useIssues } from "hooks/store"; // components -import { CycleIssueQuickActions } from "components/issues"; // types import { TIssue } from "@plane/types"; // constants -import { BaseListRoot } from "../base-list-root"; import { EIssueActions } from "../../types"; -import { EIssuesStoreType } from "constants/issue"; +import { BaseListRoot } from "../base-list-root"; export interface ICycleListLayout {} diff --git a/web/components/issues/issue-layouts/list/roots/draft-issue-root.tsx b/web/components/issues/issue-layouts/list/roots/draft-issue-root.tsx index e11971874..10b75b115 100644 --- a/web/components/issues/issue-layouts/list/roots/draft-issue-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/draft-issue-root.tsx @@ -1,16 +1,16 @@ import { FC, useMemo } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { ProjectIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components -import { ProjectIssueQuickActions } from "components/issues"; // types import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; // constants import { BaseListRoot } from "../base-list-root"; -import { EIssuesStoreType } from "constants/issue"; export const DraftIssueListLayout: FC = observer(() => { const router = useRouter(); diff --git a/web/components/issues/issue-layouts/list/roots/module-root.tsx b/web/components/issues/issue-layouts/list/roots/module-root.tsx index 95c62d34c..aca528a6a 100644 --- a/web/components/issues/issue-layouts/list/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/module-root.tsx @@ -1,16 +1,16 @@ import React, { useMemo } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // mobx store +import { ModuleIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components -import { ModuleIssueQuickActions } from "components/issues"; // types import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; // constants import { BaseListRoot } from "../base-list-root"; -import { EIssuesStoreType } from "constants/issue"; export interface IModuleListLayout {} diff --git a/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx index fa4a05bbc..dc0c68cd8 100644 --- a/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx @@ -1,17 +1,17 @@ import { FC, useMemo } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { ProjectIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; import { useIssues, useUser } from "hooks/store"; // components -import { ProjectIssueQuickActions } from "components/issues"; // types import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; // constants import { BaseListRoot } from "../base-list-root"; -import { EUserProjectRoles } from "constants/project"; -import { EIssuesStoreType } from "constants/issue"; export const ProfileIssuesListLayout: FC = observer(() => { // router diff --git a/web/components/issues/issue-layouts/list/roots/project-root.tsx b/web/components/issues/issue-layouts/list/roots/project-root.tsx index 9e1b5830b..8a0935979 100644 --- a/web/components/issues/issue-layouts/list/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/project-root.tsx @@ -1,16 +1,16 @@ import { FC, useMemo } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { ProjectIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components -import { ProjectIssueQuickActions } from "components/issues"; // types import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; // constants import { BaseListRoot } from "../base-list-root"; -import { EIssuesStoreType } from "constants/issue"; export const ListLayout: FC = observer(() => { const router = useRouter(); diff --git a/web/components/issues/issue-layouts/list/roots/project-view-root.tsx b/web/components/issues/issue-layouts/list/roots/project-view-root.tsx index 5ecfd6da2..82ca03d42 100644 --- a/web/components/issues/issue-layouts/list/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/project-view-root.tsx @@ -2,15 +2,15 @@ import React from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // store +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // constants -import { EIssuesStoreType } from "constants/issue"; // types -import { EIssueActions } from "../../types"; import { TIssue } from "@plane/types"; +import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; +import { EIssueActions } from "../../types"; // components import { BaseListRoot } from "../base-list-root"; -import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; export interface IViewListLayout { issueActions: { diff --git a/web/components/issues/issue-layouts/properties/all-properties.tsx b/web/components/issues/issue-layouts/properties/all-properties.tsx index 238d2e744..8b3e2e673 100644 --- a/web/components/issues/issue-layouts/properties/all-properties.tsx +++ b/web/components/issues/issue-layouts/properties/all-properties.tsx @@ -1,14 +1,10 @@ import { useCallback, useMemo } from "react"; +import xor from "lodash/xor"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { CalendarCheck2, CalendarClock, Layers, Link, Paperclip } from "lucide-react"; -import xor from "lodash/xor"; // hooks -import { useEventTracker, useEstimate, useLabel, useIssues, useProjectState } from "hooks/store"; -// components -import { IssuePropertyLabels } from "../properties/labels"; import { Tooltip } from "@plane/ui"; -import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; import { DateDropdown, EstimateDropdown, @@ -18,15 +14,19 @@ import { CycleDropdown, StateDropdown, } from "components/dropdowns"; -// helpers -import { renderFormattedPayloadDate } from "helpers/date-time.helper"; -import { shouldHighlightIssueDueDate } from "helpers/issue.helper"; -import { cn } from "helpers/common.helper"; -// types -import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types"; -// constants import { ISSUE_UPDATED } from "constants/event-tracker"; import { EIssuesStoreType } from "constants/issue"; +import { cn } from "helpers/common.helper"; +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { shouldHighlightIssueDueDate } from "helpers/issue.helper"; +import { useEventTracker, useEstimate, useLabel, useIssues, useProjectState } from "hooks/store"; +// components +import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types"; +import { IssuePropertyLabels } from "../properties/labels"; +import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; +// helpers +// types +// constants export interface IIssueProperties { issue: TIssue; @@ -338,7 +338,7 @@ export const IssueProperties: React.FC = observer((props) => { disabled={isReadOnly} multiple buttonVariant="border-with-text" - showCount={true} + showCount showTooltip />
    diff --git a/web/components/issues/issue-layouts/properties/labels.tsx b/web/components/issues/issue-layouts/properties/labels.tsx index 0c1091d39..a57c60d6f 100644 --- a/web/components/issues/issue-layouts/properties/labels.tsx +++ b/web/components/issues/issue-layouts/properties/labels.tsx @@ -1,16 +1,16 @@ import { Fragment, useEffect, useRef, useState } from "react"; +import { Placement } from "@popperjs/core"; import { observer } from "mobx-react-lite"; import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; import { Check, ChevronDown, Search, Tags } from "lucide-react"; // hooks +import { Tooltip } from "@plane/ui"; import { useApplication, useLabel } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components -import { Combobox } from "@headlessui/react"; -import { Tooltip } from "@plane/ui"; // types -import { Placement } from "@popperjs/core"; import { IIssueLabel } from "@plane/types"; export interface IIssuePropertyLabels { @@ -56,7 +56,7 @@ export const IssuePropertyLabels: React.FC = observer((pro // popper-js refs const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); - const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(false); // store hooks const { router: { workspaceSlug }, @@ -149,7 +149,7 @@ export const IssuePropertyLabels: React.FC = observer((pro {projectLabels ?.filter((l) => value.includes(l?.id)) .map((label) => ( - +
    = observer((props) => { const { diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx index a30db3a82..dae88a387 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx @@ -2,17 +2,19 @@ import { useState } from "react"; import { useRouter } from "next/router"; import { ExternalLink, Link, RotateCcw, Trash2 } from "lucide-react"; // hooks -import { useEventTracker, useIssues, useUser } from "hooks/store"; -// ui import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; -// components + import { DeleteIssueModal } from "components/issues"; -// helpers +// ui +// components +import { EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; import { copyUrlToClipboard } from "helpers/string.helper"; +import { useEventTracker, useIssues, useUser } from "hooks/store"; +// components +// helpers // types import { IQuickActionProps } from "../list/list-view-types"; -import { EUserProjectRoles } from "constants/project"; -import { EIssuesStoreType } from "constants/issue"; export const ArchivedIssueQuickActions: React.FC = (props) => { const { issue, handleDelete, handleRestore, customActionButton, portalElement, readOnly = false } = props; diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx index 2b4a5fa05..89beda00c 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx @@ -1,24 +1,25 @@ import { useState } from "react"; -import { useRouter } from "next/router"; import omit from "lodash/omit"; import { observer } from "mobx-react"; +import { useRouter } from "next/router"; // hooks -import { useEventTracker, useIssues, useProjectState, useUser } from "hooks/store"; // ui +import { Copy, ExternalLink, Link, Pencil, Trash2, XCircle } from "lucide-react"; import { ArchiveIcon, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; // icons -import { Copy, ExternalLink, Link, Pencil, Trash2, XCircle } from "lucide-react"; // components import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; -// helpers +import { EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { STATE_GROUPS } from "constants/state"; import { copyUrlToClipboard } from "helpers/string.helper"; +import { useEventTracker, useIssues, useProjectState, useUser } from "hooks/store"; +// components +// helpers // types import { TIssue } from "@plane/types"; import { IQuickActionProps } from "../list/list-view-types"; // constants -import { EIssuesStoreType } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { STATE_GROUPS } from "constants/state"; export const CycleIssueQuickActions: React.FC = observer((props) => { const { diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx index cf090385d..26eb6997c 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx @@ -1,23 +1,24 @@ import { useState } from "react"; -import { useRouter } from "next/router"; import omit from "lodash/omit"; import { observer } from "mobx-react"; +import { useRouter } from "next/router"; // hooks -import { useIssues, useEventTracker, useUser, useProjectState } from "hooks/store"; // ui -import { ArchiveIcon, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; import { Copy, ExternalLink, Link, Pencil, Trash2, XCircle } from "lucide-react"; +import { ArchiveIcon, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; // components import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; -// helpers +import { EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { STATE_GROUPS } from "constants/state"; import { copyUrlToClipboard } from "helpers/string.helper"; +import { useIssues, useEventTracker, useUser, useProjectState } from "hooks/store"; +// components +// helpers // types import { TIssue } from "@plane/types"; import { IQuickActionProps } from "../list/list-view-types"; // constants -import { EIssuesStoreType } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { STATE_GROUPS } from "constants/state"; export const ModuleIssueQuickActions: React.FC = observer((props) => { const { diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx index 7afbd2421..33b73f88c 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx @@ -1,23 +1,23 @@ import { useState } from "react"; -import { useRouter } from "next/router"; import omit from "lodash/omit"; import { observer } from "mobx-react"; +import { useRouter } from "next/router"; // hooks +import { Copy, ExternalLink, Link, Pencil, Trash2 } from "lucide-react"; +import { ArchiveIcon, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; +import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { STATE_GROUPS } from "constants/state"; +import { copyUrlToClipboard } from "helpers/string.helper"; import { useEventTracker, useIssues, useProjectState, useUser } from "hooks/store"; // ui -import { ArchiveIcon, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; -import { Copy, ExternalLink, Link, Pencil, Trash2 } from "lucide-react"; // components -import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; // helpers -import { copyUrlToClipboard } from "helpers/string.helper"; // types import { TIssue } from "@plane/types"; import { IQuickActionProps } from "../list/list-view-types"; // constant -import { EUserProjectRoles } from "constants/project"; -import { EIssuesStoreType } from "constants/issue"; -import { STATE_GROUPS } from "constants/state"; export const ProjectIssueQuickActions: React.FC = observer((props) => { const { diff --git a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx index 3b098c8a1..84101542f 100644 --- a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx @@ -1,31 +1,31 @@ import React, { Fragment, useCallback, useMemo } from "react"; -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; -import useSWR from "swr"; import isEmpty from "lodash/isEmpty"; +import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { useTheme } from "next-themes"; +import useSWR from "swr"; // hooks -import { useApplication, useEventTracker, useGlobalView, useIssues, useProject, useUser } from "hooks/store"; -import { useWorkspaceIssueProperties } from "hooks/use-workspace-issue-properties"; -// components +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { GlobalViewsAppliedFiltersRoot, IssuePeekOverview } from "components/issues"; import { SpreadsheetView } from "components/issues/issue-layouts"; import { AllIssueQuickActions } from "components/issues/issue-layouts/quick-action-dropdowns"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { SpreadsheetLayoutLoader } from "components/ui"; +import { ALL_ISSUES_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { EUserWorkspaceRoles } from "constants/workspace"; +import { useApplication, useEventTracker, useGlobalView, useIssues, useProject, useUser } from "hooks/store"; +import { useWorkspaceIssueProperties } from "hooks/use-workspace-issue-properties"; +// components // types import { TIssue, IIssueDisplayFilterOptions } from "@plane/types"; import { EIssueActions } from "../types"; // constants -import { EUserProjectRoles } from "constants/project"; -import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { ALL_ISSUES_EMPTY_STATE_DETAILS } from "constants/empty-state"; export const AllIssueLayoutRoot: React.FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug, globalViewId } = router.query; + const { workspaceSlug, globalViewId, ...routeFilters } = router.query; // theme const { resolvedTheme } = useTheme(); //swr hook for fetching issue properties @@ -61,14 +61,10 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { globalViewId && ["all-issues", "assigned", "created", "subscribed"].includes(globalViewId.toString()) ) { - const routerQueryParams = { ...router.query }; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { ["workspaceSlug"]: _workspaceSlug, ["globalViewId"]: _globalViewId, ...filters } = routerQueryParams; - let issueFilters: any = {}; - Object.keys(filters).forEach((key) => { + Object.keys(routeFilters).forEach((key) => { const filterKey: any = key; - const filterValue = filters[key]?.toString() || undefined; + const filterValue = routeFilters[key]?.toString() || undefined; if ( ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet.filters.includes(filterKey) && filterKey && @@ -77,7 +73,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { issueFilters = { ...issueFilters, [filterKey]: filterValue.split(",") }; }); - if (!isEmpty(filters)) + if (!isEmpty(routeFilters)) updateFilters( workspaceSlug.toString(), undefined, diff --git a/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx index 7db9a1e3b..ae8ca400a 100644 --- a/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx @@ -1,9 +1,8 @@ import React, { Fragment } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; // mobx store -import { useIssues } from "hooks/store"; // components import { ArchivedIssueListLayout, @@ -11,9 +10,10 @@ import { ProjectArchivedEmptyState, IssuePeekOverview, } from "components/issues"; +import { ListLayoutLoader } from "components/ui"; import { EIssuesStoreType } from "constants/issue"; // ui -import { ListLayoutLoader } from "components/ui"; +import { useIssues } from "hooks/store"; export const ArchivedIssueLayoutRoot: React.FC = observer(() => { // router diff --git a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx index 759495284..5f308fbd1 100644 --- a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx @@ -1,12 +1,12 @@ import React, { Fragment, useState } from "react"; -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; -import useSWR from "swr"; -import size from "lodash/size"; import isEmpty from "lodash/isEmpty"; +import size from "lodash/size"; +import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; +import useSWR from "swr"; // hooks -import { useCycle, useIssues } from "hooks/store"; // components +import { TransferIssues, TransferIssuesModal } from "components/cycles"; import { CycleAppliedFiltersRoot, CycleCalendarLayout, @@ -17,10 +17,10 @@ import { CycleSpreadsheetLayout, IssuePeekOverview, } from "components/issues"; -import { TransferIssues, TransferIssuesModal } from "components/cycles"; import { ActiveLoader } from "components/ui"; // constants import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { useCycle, useIssues } from "hooks/store"; // types import { IIssueFilterOptions } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx index 02b666ceb..1a1602ad1 100644 --- a/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx @@ -1,19 +1,19 @@ import React from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; // hooks -import { useIssues } from "hooks/store"; -// components -import { DraftIssueAppliedFiltersRoot } from "../filters/applied-filters/roots/draft-issue"; -import { DraftIssueListLayout } from "../list/roots/draft-issue-root"; -import { ProjectDraftEmptyState } from "../empty-states"; import { IssuePeekOverview } from "components/issues/peek-overview"; import { ActiveLoader } from "components/ui"; -// ui -import { DraftKanBanLayout } from "../kanban/roots/draft-issue-root"; -// constants import { EIssuesStoreType } from "constants/issue"; +import { useIssues } from "hooks/store"; +// components +import { ProjectDraftEmptyState } from "../empty-states"; +import { DraftIssueAppliedFiltersRoot } from "../filters/applied-filters/roots/draft-issue"; +import { DraftKanBanLayout } from "../kanban/roots/draft-issue-root"; +import { DraftIssueListLayout } from "../list/roots/draft-issue-root"; +// ui +// constants export const DraftIssueLayoutRoot: React.FC = observer(() => { // router diff --git a/web/components/issues/issue-layouts/roots/module-layout-root.tsx b/web/components/issues/issue-layouts/roots/module-layout-root.tsx index 14505c65a..0c6ba3b66 100644 --- a/web/components/issues/issue-layouts/roots/module-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/module-layout-root.tsx @@ -1,10 +1,9 @@ import React, { Fragment } from "react"; -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; -import useSWR from "swr"; import size from "lodash/size"; +import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; +import useSWR from "swr"; // mobx store -import { useIssues } from "hooks/store"; // components import { IssuePeekOverview, @@ -19,6 +18,7 @@ import { import { ActiveLoader } from "components/ui"; // constants import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { useIssues } from "hooks/store"; // types import { IIssueFilterOptions } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/roots/project-layout-root.tsx b/web/components/issues/issue-layouts/roots/project-layout-root.tsx index cae73610e..a57d73b2c 100644 --- a/web/components/issues/issue-layouts/roots/project-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/project-layout-root.tsx @@ -1,8 +1,10 @@ import { FC, Fragment } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; // components +// ui +import { Spinner } from "@plane/ui"; import { ListLayout, CalendarLayout, @@ -13,14 +15,12 @@ import { ProjectEmptyState, IssuePeekOverview, } from "components/issues"; -// ui -import { Spinner } from "@plane/ui"; // hooks -import { useIssues } from "hooks/store"; // helpers import { ActiveLoader } from "components/ui"; // constants import { EIssuesStoreType } from "constants/issue"; +import { useIssues } from "hooks/store"; export const ProjectLayoutRoot: FC = observer(() => { // router diff --git a/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx b/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx index fa942b7f6..dbd6c5f96 100644 --- a/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx @@ -1,9 +1,8 @@ import React, { Fragment, useMemo } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; // mobx store -import { useIssues } from "hooks/store"; // components import { IssuePeekOverview, @@ -18,6 +17,7 @@ import { import { ActiveLoader } from "components/ui"; // constants import { EIssuesStoreType } from "constants/issue"; +import { useIssues } from "hooks/store"; // types import { TIssue } from "@plane/types"; import { EIssueActions } from "../types"; diff --git a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx index 2f09b55d6..5a522a527 100644 --- a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx @@ -1,21 +1,21 @@ import { FC, useCallback } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { EIssueFilterType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; import { useUser } from "hooks/store"; // views -import { SpreadsheetView } from "./spreadsheet-view"; // types -import { TIssue, IIssueDisplayFilterOptions, TUnGroupedIssues } from "@plane/types"; -import { EIssueActions } from "../types"; -import { IQuickActionProps } from "../list/list-view-types"; // constants -import { EUserProjectRoles } from "constants/project"; import { ICycleIssuesFilter, ICycleIssues } from "store/issue/cycle"; import { IModuleIssuesFilter, IModuleIssues } from "store/issue/module"; import { IProjectIssuesFilter, IProjectIssues } from "store/issue/project"; import { IProjectViewIssuesFilter, IProjectViewIssues } from "store/issue/project-views"; -import { EIssueFilterType } from "constants/issue"; +import { TIssue, IIssueDisplayFilterOptions, TUnGroupedIssues } from "@plane/types"; +import { IQuickActionProps } from "../list/list-view-types"; +import { EIssueActions } from "../types"; +import { SpreadsheetView } from "./spreadsheet-view"; interface IBaseSpreadsheetRoot { issueFiltersStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; @@ -90,7 +90,7 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { viewId ); }, - [issueFiltersStore?.updateFilters, projectId, workspaceSlug, viewId] + [issueFiltersStore, projectId, workspaceSlug, viewId] ); const renderQuickActions = useCallback( diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/cycle-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/cycle-column.tsx index 88fbf1054..658e9c79b 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/cycle-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/cycle-column.tsx @@ -1,14 +1,14 @@ import React, { useCallback } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { CycleDropdown } from "components/dropdowns"; +import { EIssuesStoreType } from "constants/issue"; import { useEventTracker, useIssues } from "hooks/store"; // components -import { CycleDropdown } from "components/dropdowns"; // types import { TIssue } from "@plane/types"; // constants -import { EIssuesStoreType } from "constants/issue"; type Props = { issue: TIssue; diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx index e261797af..adc4a971b 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx @@ -2,13 +2,13 @@ import React from "react"; import { observer } from "mobx-react-lite"; import { CalendarCheck2 } from "lucide-react"; // hooks -import { useProjectState } from "hooks/store"; // components import { DateDropdown } from "components/dropdowns"; // helpers +import { cn } from "helpers/common.helper"; import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { shouldHighlightIssueDueDate } from "helpers/issue.helper"; -import { cn } from "helpers/common.helper"; +import { useProjectState } from "hooks/store"; // types import { TIssue } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx index f7a472b49..8143be214 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx @@ -1,6 +1,6 @@ // components -import { EstimateDropdown } from "components/dropdowns"; import { observer } from "mobx-react-lite"; +import { EstimateDropdown } from "components/dropdowns"; // types import { TIssue } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx index ac06525df..6c59c22af 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx @@ -1,5 +1,4 @@ //ui -import { CustomMenu } from "@plane/ui"; import { ArrowDownWideNarrow, ArrowUpNarrowWide, @@ -9,12 +8,13 @@ import { ListFilter, MoveRight, } from "lucide-react"; +import { CustomMenu } from "@plane/ui"; //hooks +import { SPREADSHEET_PROPERTY_DETAILS } from "constants/spreadsheet"; import useLocalStorage from "hooks/use-local-storage"; //types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssueOrderByOptions } from "@plane/types"; //constants -import { SPREADSHEET_PROPERTY_DETAILS } from "constants/spreadsheet"; interface Props { property: keyof IIssueDisplayProperties; diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx index 60e429c9f..1e6ae197a 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx @@ -1,11 +1,11 @@ import React from "react"; import { observer } from "mobx-react-lite"; // components -import { IssuePropertyLabels } from "../../properties"; // hooks import { useLabel } from "hooks/store"; // types import { TIssue } from "@plane/types"; +import { IssuePropertyLabels } from "../../properties"; type Props = { issue: TIssue; diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/module-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/module-column.tsx index c688c6e1d..67c72d2a8 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/module-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/module-column.tsx @@ -1,15 +1,15 @@ import React, { useCallback } from "react"; -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import xor from "lodash/xor"; +import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { ModuleDropdown } from "components/dropdowns"; +import { EIssuesStoreType } from "constants/issue"; import { useEventTracker, useIssues } from "hooks/store"; // components -import { ModuleDropdown } from "components/dropdowns"; // types import { TIssue } from "@plane/types"; // constants -import { EIssuesStoreType } from "constants/issue"; type Props = { issue: TIssue; @@ -71,7 +71,7 @@ export const SpreadsheetModuleColumn: React.FC = observer((props) => { buttonClassName="relative border-[0.5px] border-custom-border-400 h-4.5" onClose={onClose} multiple - showCount={true} + showCount showTooltip />
    diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx index b8801559c..714134d0c 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx @@ -8,7 +8,7 @@ import { TIssue } from "@plane/types"; type Props = { issue: TIssue; onClose: () => void; - onChange: (issue: TIssue, data: Partial,updates:any) => void; + onChange: (issue: TIssue, data: Partial, updates: any) => void; disabled: boolean; }; diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx index c635ca85e..85e294641 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx @@ -2,11 +2,11 @@ import React from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks +import { cn } from "helpers/common.helper"; import { useApplication } from "hooks/store"; // types import { TIssue } from "@plane/types"; // helpers -import { cn } from "helpers/common.helper"; type Props = { issue: TIssue; diff --git a/web/components/issues/issue-layouts/spreadsheet/issue-column.tsx b/web/components/issues/issue-layouts/spreadsheet/issue-column.tsx index 3ce70868d..01be9fe99 100644 --- a/web/components/issues/issue-layouts/spreadsheet/issue-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/issue-column.tsx @@ -1,14 +1,14 @@ import { useRef } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; // types +import { SPREADSHEET_PROPERTY_DETAILS } from "constants/spreadsheet"; +import { useEventTracker } from "hooks/store"; import { IIssueDisplayProperties, TIssue } from "@plane/types"; +import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; import { EIssueActions } from "../types"; // constants -import { SPREADSHEET_PROPERTY_DETAILS } from "constants/spreadsheet"; // components -import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; -import { useEventTracker } from "hooks/store"; -import { observer } from "mobx-react"; type Props = { displayProperties: IIssueDisplayProperties; diff --git a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx index abf6c3a01..161aa07ae 100644 --- a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx @@ -1,24 +1,25 @@ import { Dispatch, MutableRefObject, SetStateAction, useRef, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // icons import { ChevronRight, MoreHorizontal } from "lucide-react"; -// constants -import { SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet"; -// components -import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; -import RenderIfVisible from "components/core/render-if-visible-HOC"; -import { IssueColumn } from "./issue-column"; // ui import { ControlLink, Tooltip } from "@plane/ui"; -// hooks -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -import { useIssueDetail, useProject } from "hooks/store"; +// components +import RenderIfVisible from "components/core/render-if-visible-HOC"; +// constants +import { SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet"; // helper import { cn } from "helpers/common.helper"; +// hooks +import { useIssueDetail, useProject } from "hooks/store"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; // types import { IIssueDisplayProperties, TIssue } from "@plane/types"; +// local components +import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; import { EIssueActions } from "../types"; +import { IssueColumn } from "./issue-column"; interface Props { displayProperties: IIssueDisplayProperties; @@ -255,6 +256,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => { {/* Rest of the columns */} {SPREADSHEET_PROPERTY_LIST.map((property) => ( { const router = useRouter(); diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx index af8abc801..c52b40527 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx @@ -2,13 +2,13 @@ import React, { useMemo } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // mobx store +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components -import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; -import { EIssueActions } from "../../types"; import { TIssue } from "@plane/types"; import { ModuleIssueQuickActions } from "../../quick-action-dropdowns"; -import { EIssuesStoreType } from "constants/issue"; +import { EIssueActions } from "../../types"; +import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; export const ModuleSpreadsheetLayout: React.FC = observer(() => { const router = useRouter(); diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx index 4ce54cff5..cc570fd81 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx @@ -2,13 +2,13 @@ import React, { useMemo } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // mobx store +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; -import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; -import { EIssueActions } from "../../types"; import { TIssue } from "@plane/types"; import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; -import { EIssuesStoreType } from "constants/issue"; +import { EIssueActions } from "../../types"; +import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; export const ProjectSpreadsheetLayout: React.FC = observer(() => { const router = useRouter(); diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx index d8b7571e5..dd134e070 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx @@ -2,15 +2,15 @@ import React from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // mobx store +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components -import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; -import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; -// types -import { EIssueActions } from "../../types"; import { TIssue } from "@plane/types"; +import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; +import { EIssueActions } from "../../types"; +import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; +// types // constants -import { EIssuesStoreType } from "constants/issue"; export interface IViewSpreadsheetLayout { issueActions: { diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header-column.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header-column.tsx index 4401eb839..346846def 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header-column.tsx @@ -1,10 +1,10 @@ import { useRef } from "react"; //types +import { observer } from "mobx-react"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; //components import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; import { HeaderColumn } from "./columns/header-column"; -import { observer } from "mobx-react"; interface Props { displayProperties: IIssueDisplayProperties; diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx index 98666d790..ea0e0f1c2 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx @@ -1,9 +1,9 @@ // ui import { LayersIcon } from "@plane/ui"; // types +import { SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; // constants -import { SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet"; // components import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; import { SpreadsheetHeaderColumn } from "./spreadsheet-header-column"; @@ -38,6 +38,7 @@ export const SpreadsheetHeader = (props: Props) => { {SPREADSHEET_PROPERTY_LIST.map((property) => ( { // states const isScrolled = useRef(false); - const handleScroll = () => { + const handleScroll = useCallback(() => { if (!containerRef.current) return; const scrollLeft = containerRef.current.scrollLeft; @@ -51,19 +51,19 @@ export const SpreadsheetTable = observer((props: Props) => { //The shadow styles are added this way to avoid re-render of all the rows of table, which could be costly if (scrollLeft > 0 !== isScrolled.current) { - const firtColumns = containerRef.current.querySelectorAll("table tr td:first-child, th:first-child"); + const firstColumns = containerRef.current.querySelectorAll("table tr td:first-child, th:first-child"); - for (let i = 0; i < firtColumns.length; i++) { + for (let i = 0; i < firstColumns.length; i++) { const shadow = i === 0 ? headerShadow : columnShadow; if (scrollLeft > 0) { - (firtColumns[i] as HTMLElement).style.boxShadow = shadow; + (firstColumns[i] as HTMLElement).style.boxShadow = shadow; } else { - (firtColumns[i] as HTMLElement).style.boxShadow = "none"; + (firstColumns[i] as HTMLElement).style.boxShadow = "none"; } } isScrolled.current = scrollLeft > 0; } - }; + }, [containerRef]); useEffect(() => { const currentContainerRef = containerRef.current; @@ -73,7 +73,7 @@ export const SpreadsheetTable = observer((props: Props) => { return () => { if (currentContainerRef) currentContainerRef.removeEventListener("scroll", handleScroll); }; - }, []); + }, [handleScroll, containerRef]); const handleKeyBoardNavigation = useTableKeyboardNavigation(); diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx index e7b2bcee6..f71634ab8 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx @@ -3,12 +3,12 @@ import { observer } from "mobx-react-lite"; // components import { Spinner } from "@plane/ui"; import { SpreadsheetQuickAddIssueForm } from "components/issues"; -import { SpreadsheetTable } from "./spreadsheet-table"; -// types +import { useProject } from "hooks/store"; import { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; import { EIssueActions } from "../types"; +import { SpreadsheetTable } from "./spreadsheet-table"; +// types //hooks -import { useProject } from "hooks/store"; type Props = { displayProperties: IIssueDisplayProperties; diff --git a/web/components/issues/issue-layouts/utils.tsx b/web/components/issues/issue-layouts/utils.tsx index ce49d774d..6dd462fd1 100644 --- a/web/components/issues/issue-layouts/utils.tsx +++ b/web/components/issues/issue-layouts/utils.tsx @@ -1,19 +1,19 @@ +import { ContrastIcon } from "lucide-react"; import { Avatar, CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/ui"; // stores +import { ISSUE_PRIORITIES } from "constants/issue"; +import { STATE_GROUPS } from "constants/state"; +import { renderEmoji } from "helpers/emoji.helper"; +import { ICycleStore } from "store/cycle.store"; +import { ILabelStore } from "store/label.store"; import { IMemberRootStore } from "store/member"; +import { IModuleStore } from "store/module.store"; import { IProjectStore } from "store/project/project.store"; import { IStateStore } from "store/state.store"; -import { ILabelStore } from "store/label.store"; -import { ICycleStore } from "store/cycle.store"; -import { IModuleStore } from "store/module.store"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; // constants -import { STATE_GROUPS } from "constants/state"; -import { ISSUE_PRIORITIES } from "constants/issue"; // types import { GroupByColumnTypes, IGroupByColumn, TCycleGroups } from "@plane/types"; -import { ContrastIcon } from "lucide-react"; export const getGroupByColumns = ( groupBy: GroupByColumnTypes | null, @@ -62,7 +62,7 @@ const getProjectColumns = (project: IProjectStore): IGroupByColumn[] | undefined return { id: project.id, name: project.name, - icon:
    {renderEmoji(project.emoji || "")}
    , + icon:
    {renderEmoji(project.emoji || "")}
    , payload: { project_id: project.id }, }; }) as any; @@ -112,19 +112,19 @@ const getModuleColumns = (projectStore: IProjectStore, moduleStore: IModuleStore const modules = []; moduleIds.map((moduleId) => { - const _module = getModuleById(moduleId); - if (_module) + const moduleInfo = getModuleById(moduleId); + if (moduleInfo) modules.push({ - id: _module.id, - name: _module.name, - icon: , - payload: { module_ids: [_module.id] }, + id: moduleInfo.id, + name: moduleInfo.name, + icon: , + payload: { module_ids: [moduleInfo.id] }, }); }) as any; modules.push({ id: "None", name: "None", - icon: , + icon: , }); return modules as any; @@ -138,7 +138,7 @@ const getStateColumns = (projectState: IStateStore): IGroupByColumn[] | undefine id: state.id, name: state.name, icon: ( -
    +
    ), @@ -153,7 +153,7 @@ const getStateGroupColumns = () => { id: stateGroup.key, name: stateGroup.label, icon: ( -
    +
    ), @@ -183,7 +183,7 @@ const getLabelsColumns = (label: ILabelStore) => { id: label.id, name: label.name, icon: ( -
    +
    ), payload: label?.id === "None" ? {} : { label_ids: [label.id] }, })); diff --git a/web/components/issues/issue-modal/draft-issue-layout.tsx b/web/components/issues/issue-modal/draft-issue-layout.tsx index b4dae211d..785ccb0bb 100644 --- a/web/components/issues/issue-modal/draft-issue-layout.tsx +++ b/web/components/issues/issue-modal/draft-issue-layout.tsx @@ -1,15 +1,15 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { TOAST_TYPE, setToast } from "@plane/ui"; +import { ConfirmIssueDiscard } from "components/issues"; +import { IssueFormRoot } from "components/issues/issue-modal/form"; import { useEventTracker } from "hooks/store"; // services import { IssueDraftService } from "services/issue"; // ui -import { TOAST_TYPE, setToast } from "@plane/ui"; // components -import { IssueFormRoot } from "components/issues/issue-modal/form"; -import { ConfirmIssueDiscard } from "components/issues"; // types import type { TIssue } from "@plane/types"; diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx index 7fcb6cffa..527ebd0e1 100644 --- a/web/components/issues/issue-modal/form.tsx +++ b/web/components/issues/issue-modal/form.tsx @@ -1,20 +1,13 @@ import React, { FC, useState, useRef, useEffect, Fragment } from "react"; -import { useRouter } from "next/router"; +import { RichTextEditorWithRef } from "@plane/rich-text-editor"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; import { LayoutPanelTop, Sparkle, X } from "lucide-react"; // editor -import { RichTextEditorWithRef } from "@plane/rich-text-editor"; // hooks -import { useApplication, useEstimate, useIssueDetail, useMention, useProject, useWorkspace } from "hooks/store"; -// services -import { AIService } from "services/ai.service"; -import { FileService } from "services/file.service"; -// components +import { Button, CustomMenu, Input, Loader, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; import { GptAssistantPopover } from "components/core"; -import { ParentIssuesListModal } from "components/issues"; -import { IssueLabelSelect } from "components/issues/select"; -import { CreateLabelModal } from "components/labels"; import { CycleDropdown, DateDropdown, @@ -25,10 +18,17 @@ import { MemberDropdown, StateDropdown, } from "components/dropdowns"; -// ui -import { Button, CustomMenu, Input, Loader, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; -// helpers +import { ParentIssuesListModal } from "components/issues"; +import { IssueLabelSelect } from "components/issues/select"; +import { CreateLabelModal } from "components/labels"; import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { useApplication, useEstimate, useIssueDetail, useMention, useProject, useWorkspace } from "hooks/store"; +// services +import { AIService } from "services/ai.service"; +import { FileService } from "services/file.service"; +// components +// ui +// helpers // types import type { TIssue, ISearchIssueResponse } from "@plane/types"; @@ -360,14 +360,14 @@ export const IssueFormRoot: FC = observer((props) => { ref={ref} hasError={Boolean(errors.name)} placeholder="Issue Title" - className="resize-none text-xl w-full" + className="w-full resize-none text-xl" tabIndex={getTabIndex("name")} /> )} />
    {data?.description_html === undefined ? ( - +
    @@ -381,18 +381,18 @@ export const IssueFormRoot: FC = observer((props) => {
    -
    +
    ) : ( -
    +
    {issueName && issueName.trim() !== "" && envConfig?.has_openai_configured && ( diff --git a/web/components/issues/parent-issues-list-modal.tsx b/web/components/issues/parent-issues-list-modal.tsx index b97eafc06..f5b804e74 100644 --- a/web/components/issues/parent-issues-list-modal.tsx +++ b/web/components/issues/parent-issues-list-modal.tsx @@ -1,17 +1,15 @@ import React, { useEffect, useState } from "react"; - import { useRouter } from "next/router"; - // headless ui import { Combobox, Dialog, Transition } from "@headlessui/react"; // services +import { Rocket, Search } from "lucide-react"; +import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui"; +import useDebounce from "hooks/use-debounce"; import { ProjectService } from "services/project"; // hooks -import useDebounce from "hooks/use-debounce"; // ui -import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui"; // icons -import { Rocket, Search } from "lucide-react"; // types import { ISearchIssueResponse } from "@plane/types"; diff --git a/web/components/issues/peek-overview/header.tsx b/web/components/issues/peek-overview/header.tsx index 8d8ec00df..b47551bc6 100644 --- a/web/components/issues/peek-overview/header.tsx +++ b/web/components/issues/peek-overview/header.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react"; +import { useRouter } from "next/router"; import { MoveRight, MoveDiagonal, Link2, Trash2, RotateCcw } from "lucide-react"; // ui import { @@ -13,15 +13,17 @@ import { TOAST_TYPE, setToast, } from "@plane/ui"; +// components +import { IssueSubscription, IssueUpdateStatus } from "components/issues"; +import { STATE_GROUPS } from "constants/state"; // helpers +import { cn } from "helpers/common.helper"; import { copyUrlToClipboard } from "helpers/string.helper"; // store hooks import { useIssueDetail, useProjectState, useUser } from "hooks/store"; // helpers -import { cn } from "helpers/common.helper"; // components -import { IssueSubscription, IssueUpdateStatus } from "components/issues"; -import { STATE_GROUPS } from "constants/state"; +// helpers export type TPeekModes = "side-peek" | "modal" | "full-screen"; diff --git a/web/components/issues/peek-overview/issue-detail.tsx b/web/components/issues/peek-overview/issue-detail.tsx index 7f540874c..59b1c1609 100644 --- a/web/components/issues/peek-overview/issue-detail.tsx +++ b/web/components/issues/peek-overview/issue-detail.tsx @@ -1,14 +1,14 @@ import { FC, useEffect } from "react"; import { observer } from "mobx-react"; // store hooks +import { TIssueOperations } from "components/issues"; import { useIssueDetail, useProject, useUser } from "hooks/store"; // hooks import useReloadConfirmations from "hooks/use-reload-confirmation"; // components -import { TIssueOperations } from "components/issues"; +import { IssueDescriptionInput } from "../description-input"; import { IssueReaction } from "../issue-detail/reactions"; import { IssueTitleInput } from "../title-input"; -import { IssueDescriptionInput } from "../description-input"; interface IPeekOverviewIssueDetails { workspaceSlug: string; diff --git a/web/components/issues/peek-overview/properties.tsx b/web/components/issues/peek-overview/properties.tsx index 2f5a02c11..8ae021b86 100644 --- a/web/components/issues/peek-overview/properties.tsx +++ b/web/components/issues/peek-overview/properties.tsx @@ -12,9 +12,9 @@ import { CalendarCheck2, } from "lucide-react"; // hooks -import { useIssueDetail, useProject, useProjectState } from "hooks/store"; // ui icons import { DiceIcon, DoubleCircleIcon, UserGroupIcon, ContrastIcon, RelatedIcon } from "@plane/ui"; +import { DateDropdown, EstimateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns"; import { IssueLinkRoot, IssueCycleSelect, @@ -24,12 +24,12 @@ import { TIssueOperations, IssueRelationSelect, } from "components/issues"; -import { DateDropdown, EstimateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns"; // components +import { cn } from "helpers/common.helper"; import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // helpers -import { cn } from "helpers/common.helper"; import { shouldHighlightIssueDueDate } from "helpers/issue.helper"; +import { useIssueDetail, useProject, useProjectState } from "hooks/store"; interface IPeekOverviewProperties { workspaceSlug: string; diff --git a/web/components/issues/peek-overview/root.tsx b/web/components/issues/peek-overview/root.tsx index b28cc5de6..3eae8d3e8 100644 --- a/web/components/issues/peek-overview/root.tsx +++ b/web/components/issues/peek-overview/root.tsx @@ -1,18 +1,19 @@ import { FC, useEffect, useState, useMemo } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks -import { useEventTracker, useIssueDetail, useIssues, useUser } from "hooks/store"; -// ui import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; -// components import { IssueView } from "components/issues"; +// ui +// components +import { ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED, ISSUE_RESTORED } from "constants/event-tracker"; +import { EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { useEventTracker, useIssueDetail, useIssues, useUser } from "hooks/store"; +// components // types import { TIssue } from "@plane/types"; // constants -import { EUserProjectRoles } from "constants/project"; -import { EIssuesStoreType } from "constants/issue"; -import { ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED, ISSUE_RESTORED } from "constants/event-tracker"; interface IIssuePeekOverview { is_archived?: boolean; diff --git a/web/components/issues/peek-overview/view.tsx b/web/components/issues/peek-overview/view.tsx index f94901c45..aa7bd395f 100644 --- a/web/components/issues/peek-overview/view.tsx +++ b/web/components/issues/peek-overview/view.tsx @@ -1,12 +1,7 @@ import { FC, useRef, useState } from "react"; - import { observer } from "mobx-react-lite"; - -// hooks -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -import useKeypress from "hooks/use-keypress"; -// store hooks -import { useIssueDetail } from "hooks/store"; +// ui +import { Spinner } from "@plane/ui"; // components import { DeleteIssueModal, @@ -17,9 +12,12 @@ import { TIssueOperations, ArchiveIssueModal, } from "components/issues"; +// hooks +import { useIssueDetail } from "hooks/store"; +import useKeypress from "hooks/use-keypress"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; +// store hooks import { IssueActivity } from "../issue-detail/issue-activity"; -// ui -import { Spinner } from "@plane/ui"; interface IIssueView { workspaceSlug: string; @@ -139,7 +137,7 @@ export const IssueView: FC = observer((props) => { disabled={disabled} /> {/* content */} -
    +
    {isLoading && !issue ? (
    @@ -170,7 +168,7 @@ export const IssueView: FC = observer((props) => {
    ) : ( -
    +
    >; diff --git a/web/components/issues/sub-issues/issue-list-item.tsx b/web/components/issues/sub-issues/issue-list-item.tsx index a748e986e..5d7d19730 100644 --- a/web/components/issues/sub-issues/issue-list-item.tsx +++ b/web/components/issues/sub-issues/issue-list-item.tsx @@ -1,16 +1,16 @@ import React from "react"; +import { observer } from "mobx-react-lite"; import { ChevronDown, ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } from "lucide-react"; // components +import { ControlLink, CustomMenu, Tooltip } from "@plane/ui"; +import { useIssueDetail, useProject, useProjectState } from "hooks/store"; +import { TIssue } from "@plane/types"; import { IssueList } from "./issues-list"; import { IssueProperty } from "./properties"; // ui -import { ControlLink, CustomMenu, Tooltip } from "@plane/ui"; // types -import { TIssue } from "@plane/types"; import { TSubIssueOperations } from "./root"; // import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root"; -import { useIssueDetail, useProject, useProjectState } from "hooks/store"; -import { observer } from "mobx-react-lite"; export interface ISubIssues { workspaceSlug: string; diff --git a/web/components/issues/sub-issues/issues-list.tsx b/web/components/issues/sub-issues/issues-list.tsx index ad09938cb..cb1d66461 100644 --- a/web/components/issues/sub-issues/issues-list.tsx +++ b/web/components/issues/sub-issues/issues-list.tsx @@ -3,9 +3,9 @@ import { observer } from "mobx-react-lite"; // hooks import { useIssueDetail } from "hooks/store"; // components +import { TIssue } from "@plane/types"; import { IssueListItem } from "./issue-list-item"; // types -import { TIssue } from "@plane/types"; import { TSubIssueOperations } from "./root"; export interface IIssueList { diff --git a/web/components/issues/sub-issues/properties.tsx b/web/components/issues/sub-issues/properties.tsx index 03c9d8902..f737b57e7 100644 --- a/web/components/issues/sub-issues/properties.tsx +++ b/web/components/issues/sub-issues/properties.tsx @@ -1,8 +1,8 @@ import React from "react"; // hooks +import { PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns"; import { useIssueDetail } from "hooks/store"; // components -import { PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns"; // types import { TSubIssueOperations } from "./root"; diff --git a/web/components/issues/sub-issues/root.tsx b/web/components/issues/sub-issues/root.tsx index da49200dd..ed46a40f5 100644 --- a/web/components/issues/sub-issues/root.tsx +++ b/web/components/issues/sub-issues/root.tsx @@ -1,20 +1,20 @@ import { FC, useCallback, useEffect, useMemo, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Plus, ChevronRight, ChevronDown, Loader } from "lucide-react"; // hooks -import { useEventTracker, useIssueDetail } from "hooks/store"; -// components +import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; import { ExistingIssuesListModal } from "components/core"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; +import { copyTextToClipboard } from "helpers/string.helper"; +import { useEventTracker, useIssueDetail } from "hooks/store"; +// components +import { IUser, TIssue } from "@plane/types"; import { IssueList } from "./issues-list"; import { ProgressBar } from "./progressbar"; // ui -import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; // helpers -import { copyTextToClipboard } from "helpers/string.helper"; // types -import { IUser, TIssue } from "@plane/types"; export interface ISubIssuesRoot { workspaceSlug: string; diff --git a/web/components/issues/title-input.tsx b/web/components/issues/title-input.tsx index 2db4eb4b5..bb412b795 100644 --- a/web/components/issues/title-input.tsx +++ b/web/components/issues/title-input.tsx @@ -3,9 +3,9 @@ import { observer } from "mobx-react"; // components import { TextArea } from "@plane/ui"; // types +import useDebounce from "hooks/use-debounce"; import { TIssueOperations } from "./issue-detail"; // hooks -import useDebounce from "hooks/use-debounce"; export type IssueTitleInputProps = { disabled?: boolean; diff --git a/web/components/labels/create-label-modal.tsx b/web/components/labels/create-label-modal.tsx index b6a3f63e8..ee0988741 100644 --- a/web/components/labels/create-label-modal.tsx +++ b/web/components/labels/create-label-modal.tsx @@ -1,18 +1,18 @@ import React, { useEffect } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { Controller, useForm } from "react-hook-form"; +import { useRouter } from "next/router"; import { TwitterPicker } from "react-color"; +import { Controller, useForm } from "react-hook-form"; import { Dialog, Popover, Transition } from "@headlessui/react"; import { ChevronDown } from "lucide-react"; // hooks +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; +import { LABEL_COLOR_OPTIONS, getRandomLabelColor } from "constants/label"; import { useLabel } from "hooks/store"; // ui -import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import type { IIssueLabel, IState } from "@plane/types"; // constants -import { LABEL_COLOR_OPTIONS, getRandomLabelColor } from "constants/label"; // types type Props = { diff --git a/web/components/labels/create-update-label-inline.tsx b/web/components/labels/create-update-label-inline.tsx index d30d48a6a..a29a334b6 100644 --- a/web/components/labels/create-update-label-inline.tsx +++ b/web/components/labels/create-update-label-inline.tsx @@ -1,17 +1,17 @@ import React, { forwardRef, useEffect } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { TwitterPicker } from "react-color"; import { Controller, SubmitHandler, useForm } from "react-hook-form"; import { Popover, Transition } from "@headlessui/react"; -// hooks -import { useLabel } from "hooks/store"; // ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; +// constants +import { getRandomLabelColor, LABEL_COLOR_OPTIONS } from "constants/label"; +// hooks +import { useLabel } from "hooks/store"; // types import { IIssueLabel } from "@plane/types"; -// fetch-keys -import { getRandomLabelColor, LABEL_COLOR_OPTIONS } from "constants/label"; type Props = { labelForm: boolean; @@ -74,6 +74,7 @@ export const CreateUpdateLabelInline = observer( const handleLabelUpdate: SubmitHandler = async (formData) => { if (!workspaceSlug || !projectId || isSubmitting) return; + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain await updateLabel(workspaceSlug.toString(), projectId.toString(), labelToUpdate?.id!, formData) .then(() => { reset(defaultValues); diff --git a/web/components/labels/delete-label-modal.tsx b/web/components/labels/delete-label-modal.tsx index 83b3e807d..d5c269136 100644 --- a/web/components/labels/delete-label-modal.tsx +++ b/web/components/labels/delete-label-modal.tsx @@ -1,13 +1,13 @@ import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; -import { observer } from "mobx-react-lite"; // hooks +import { AlertTriangle } from "lucide-react"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; import { useLabel } from "hooks/store"; // icons -import { AlertTriangle } from "lucide-react"; // ui -import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import type { IIssueLabel } from "@plane/types"; diff --git a/web/components/labels/label-block/label-item-block.tsx b/web/components/labels/label-block/label-item-block.tsx index eca3bcaaf..2a797d0b6 100644 --- a/web/components/labels/label-block/label-item-block.tsx +++ b/web/components/labels/label-block/label-item-block.tsx @@ -1,12 +1,12 @@ import { useRef, useState } from "react"; -import { LucideIcon, X } from "lucide-react"; import { DraggableProvidedDragHandleProps } from "@hello-pangea/dnd"; +import { LucideIcon, X } from "lucide-react"; //ui import { CustomMenu } from "@plane/ui"; //types +import useOutsideClickDetector from "hooks/use-outside-click-detector"; import { IIssueLabel } from "@plane/types"; //hooks -import useOutsideClickDetector from "hooks/use-outside-click-detector"; //components import { DragHandle } from "./drag-handle"; import { LabelName } from "./label-name"; diff --git a/web/components/labels/project-setting-label-group.tsx b/web/components/labels/project-setting-label-group.tsx index 71d11dacb..6519e581e 100644 --- a/web/components/labels/project-setting-label-group.tsx +++ b/web/components/labels/project-setting-label-group.tsx @@ -1,12 +1,4 @@ import React, { Dispatch, SetStateAction, useState } from "react"; -import { Disclosure, Transition } from "@headlessui/react"; - -// store -import { observer } from "mobx-react-lite"; -// icons -import { ChevronDown, Pencil, Trash2 } from "lucide-react"; -// types -import { IIssueLabel } from "@plane/types"; import { Draggable, DraggableProvided, @@ -14,10 +6,18 @@ import { DraggableStateSnapshot, Droppable, } from "@hello-pangea/dnd"; -import { ICustomMenuItem, LabelItemBlock } from "./label-block/label-item-block"; -import { CreateUpdateLabelInline } from "./create-update-label-inline"; -import { ProjectSettingLabelItem } from "./project-setting-label-item"; +import { observer } from "mobx-react-lite"; +import { Disclosure, Transition } from "@headlessui/react"; + +// store +// icons +import { ChevronDown, Pencil, Trash2 } from "lucide-react"; +// types import useDraggableInPortal from "hooks/use-draggable-portal"; +import { IIssueLabel } from "@plane/types"; +import { CreateUpdateLabelInline } from "./create-update-label-inline"; +import { ICustomMenuItem, LabelItemBlock } from "./label-block/label-item-block"; +import { ProjectSettingLabelItem } from "./project-setting-label-item"; type Props = { label: IIssueLabel; @@ -107,7 +107,7 @@ export const ProjectSettingLabelGroup: React.FC = observer((props) => { customMenuItems={customMenuItems} dragHandleProps={dragHandleProps} handleLabelDelete={handleLabelDelete} - isLabelGroup={true} + isLabelGroup /> )} diff --git a/web/components/labels/project-setting-label-item.tsx b/web/components/labels/project-setting-label-item.tsx index ed72e4503..30e424064 100644 --- a/web/components/labels/project-setting-label-item.tsx +++ b/web/components/labels/project-setting-label-item.tsx @@ -1,14 +1,14 @@ import React, { Dispatch, SetStateAction, useState } from "react"; -import { useRouter } from "next/router"; import { DraggableProvidedDragHandleProps, DraggableStateSnapshot } from "@hello-pangea/dnd"; +import { useRouter } from "next/router"; import { X, Pencil } from "lucide-react"; // hooks import { useLabel } from "hooks/store"; // types import { IIssueLabel } from "@plane/types"; // components -import { ICustomMenuItem, LabelItemBlock } from "./label-block/label-item-block"; import { CreateUpdateLabelInline } from "./create-update-label-inline"; +import { ICustomMenuItem, LabelItemBlock } from "./label-block/label-item-block"; type Props = { label: IIssueLabel; diff --git a/web/components/labels/project-setting-label-list.tsx b/web/components/labels/project-setting-label-list.tsx index fcd84d70a..ba6b43b0b 100644 --- a/web/components/labels/project-setting-label-list.tsx +++ b/web/components/labels/project-setting-label-list.tsx @@ -1,6 +1,4 @@ import React, { useState, useRef } from "react"; -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import { DragDropContext, Draggable, @@ -9,24 +7,26 @@ import { DropResult, Droppable, } from "@hello-pangea/dnd"; +import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { useTheme } from "next-themes"; // hooks -import { useLabel, useUser } from "hooks/store"; -import useDraggableInPortal from "hooks/use-draggable-portal"; -// components +import { Button, Loader } from "@plane/ui"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { CreateUpdateLabelInline, DeleteLabelModal, ProjectSettingLabelGroup, ProjectSettingLabelItem, } from "components/labels"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { useLabel, useUser } from "hooks/store"; +import useDraggableInPortal from "hooks/use-draggable-portal"; +// components // ui -import { Button, Loader } from "@plane/ui"; // types import { IIssueLabel } from "@plane/types"; // constants -import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; const LABELS_ROOT = "labels.root"; @@ -76,16 +76,18 @@ export const ProjectSettingsLabelList: React.FC = observer(() => { if (destination?.droppableId === LABELS_ROOT) parentLabel = null; if (result.reason == "DROP" && childLabel != parentLabel) { - updateLabelPosition( - workspaceSlug?.toString()!, - projectId?.toString()!, - childLabel, - parentLabel, - index, - prevParentLabel == parentLabel, - prevIndex - ); - return; + if (workspaceSlug && projectId) { + updateLabelPosition( + workspaceSlug?.toString(), + projectId?.toString(), + childLabel, + parentLabel, + index, + prevParentLabel == parentLabel, + prevIndex + ); + return; + } } }; @@ -104,7 +106,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
    {showLabelForm && ( -
    +
    { )} {projectLabels ? ( projectLabels.length === 0 && !showLabelForm ? ( -
    +
    = observer((props) => { }); }; - const handleUpdateModule = async (payload: Partial, dirtyFields: any) => { + const handleUpdateModule = async (payload: Partial, dirtyFields: unknown) => { if (!workspaceSlug || !projectId || !data) return; const selectedProjectId = payload.project_id ?? projectId.toString(); @@ -92,7 +92,7 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { }); captureModuleEvent({ eventName: MODULE_UPDATED, - payload: { ...res, changed_properties: Object.keys(dirtyFields), state: "SUCCESS" }, + payload: { ...res, changed_properties: Object.keys(dirtyFields || {}), state: "SUCCESS" }, }); }) .catch((err) => { @@ -108,7 +108,7 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { }); }; - const handleFormSubmit = async (formData: Partial, dirtyFields: any) => { + const handleFormSubmit = async (formData: Partial, dirtyFields: unknown) => { if (!workspaceSlug || !projectId) return; const payload: Partial = { diff --git a/web/components/modules/module-card-item.tsx b/web/components/modules/module-card-item.tsx index dbbde56d7..8023657da 100644 --- a/web/components/modules/module-card-item.tsx +++ b/web/components/modules/module-card-item.tsx @@ -1,21 +1,21 @@ import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import { Info, LinkIcon, Pencil, Star, Trash2 } from "lucide-react"; // hooks -import { useEventTracker, useMember, useModule, useUser } from "hooks/store"; -// components -import { CreateUpdateModuleModal, DeleteModuleModal } from "components/modules"; -// ui import { Avatar, AvatarGroup, CustomMenu, LayersIcon, Tooltip, TOAST_TYPE, setToast, setPromiseToast } from "@plane/ui"; -// helpers -import { copyUrlToClipboard } from "helpers/string.helper"; -import { renderFormattedDate } from "helpers/date-time.helper"; -// constants +import { CreateUpdateModuleModal, DeleteModuleModal } from "components/modules"; +import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "constants/event-tracker"; import { MODULE_STATUS } from "constants/module"; import { EUserProjectRoles } from "constants/project"; -import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "constants/event-tracker"; +import { renderFormattedDate } from "helpers/date-time.helper"; +import { copyUrlToClipboard } from "helpers/string.helper"; +import { useEventTracker, useMember, useModule, useUser } from "hooks/store"; +// components +// ui +// helpers +// constants type Props = { moduleId: string; diff --git a/web/components/modules/module-list-item.tsx b/web/components/modules/module-list-item.tsx index 63e780cb2..7fe25b918 100644 --- a/web/components/modules/module-list-item.tsx +++ b/web/components/modules/module-list-item.tsx @@ -1,13 +1,9 @@ import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import { Check, Info, LinkIcon, Pencil, Star, Trash2, User2 } from "lucide-react"; // hooks -import { useModule, useUser, useEventTracker, useMember } from "hooks/store"; -// components -import { CreateUpdateModuleModal, DeleteModuleModal } from "components/modules"; -// ui import { Avatar, AvatarGroup, @@ -18,13 +14,17 @@ import { setToast, setPromiseToast, } from "@plane/ui"; -// helpers -import { copyUrlToClipboard } from "helpers/string.helper"; -import { renderFormattedDate } from "helpers/date-time.helper"; -// constants +import { CreateUpdateModuleModal, DeleteModuleModal } from "components/modules"; +import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "constants/event-tracker"; import { MODULE_STATUS } from "constants/module"; import { EUserProjectRoles } from "constants/project"; -import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "constants/event-tracker"; +import { renderFormattedDate } from "helpers/date-time.helper"; +import { copyUrlToClipboard } from "helpers/string.helper"; +import { useModule, useUser, useEventTracker, useMember } from "hooks/store"; +// components +// ui +// helpers +// constants type Props = { moduleId: string; @@ -175,9 +175,9 @@ export const ModuleListItem: React.FC = observer((props) => { )} setDeleteModal(false)} /> -
    -
    -
    +
    +
    +
    @@ -202,10 +202,10 @@ export const ModuleListItem: React.FC = observer((props) => {
    -
    +
    {moduleStatus && ( = observer((props) => {
    -
    +
    {renderDate && ( @@ -226,7 +226,7 @@ export const ModuleListItem: React.FC = observer((props) => { )}
    -
    +
    {moduleDetails.member_ids.length > 0 ? ( diff --git a/web/components/modules/module-mobile-header.tsx b/web/components/modules/module-mobile-header.tsx index e3f504479..4763639ed 100644 --- a/web/components/modules/module-mobile-header.tsx +++ b/web/components/modules/module-mobile-header.tsx @@ -1,12 +1,12 @@ -import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; +import { useCallback, useState } from "react"; +import router from "next/router"; +import { Calendar, ChevronDown, Kanban, List } from "lucide-react"; import { CustomMenu } from "@plane/ui"; import { ProjectAnalyticsModal } from "components/analytics"; import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues"; import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "constants/issue"; import { useIssues, useLabel, useMember, useModule, useProjectState } from "hooks/store"; -import { Calendar, ChevronDown, Kanban, List } from "lucide-react"; -import router from "next/router"; -import { useCallback, useState } from "react"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; export const ModuleMobileHeader = () => { const [analyticsModal, setAnalyticsModal] = useState(false); @@ -83,35 +83,36 @@ export const ModuleMobileHeader = () => { onClose={() => setAnalyticsModal(false)} moduleDetails={moduleDetails ?? undefined} /> -
    +
    Layout} + customButton={Layout} customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm" closeOnSelect > {layouts.map((layout, index) => ( { handleLayoutChange(ISSUE_LAYOUTS[index].key); }} className="flex items-center gap-2" > - +
    {layout.title}
    ))}
    -
    +
    + Filters - + } > @@ -127,14 +128,14 @@ export const ModuleMobileHeader = () => { />
    -
    +
    + Display - + } > @@ -153,7 +154,7 @@ export const ModuleMobileHeader = () => { diff --git a/web/components/modules/module-peek-overview.tsx b/web/components/modules/module-peek-overview.tsx index 81614b61b..5590d0390 100644 --- a/web/components/modules/module-peek-overview.tsx +++ b/web/components/modules/module-peek-overview.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks import { useModule } from "hooks/store"; // components diff --git a/web/components/modules/modules-list-view.tsx b/web/components/modules/modules-list-view.tsx index bf12fde8b..33c11cbd8 100644 --- a/web/components/modules/modules-list-view.tsx +++ b/web/components/modules/modules-list-view.tsx @@ -1,17 +1,17 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { useTheme } from "next-themes"; // hooks -import { useApplication, useEventTracker, useModule, useUser } from "hooks/store"; -import useLocalStorage from "hooks/use-local-storage"; // components -import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "components/modules"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "components/modules"; // ui import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui"; // constants -import { EUserProjectRoles } from "constants/project"; import { MODULE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EUserProjectRoles } from "constants/project"; +import { useApplication, useEventTracker, useModule, useUser } from "hooks/store"; +import useLocalStorage from "hooks/use-local-storage"; export const ModulesListView: React.FC = observer(() => { // router diff --git a/web/components/modules/select/status.tsx b/web/components/modules/select/status.tsx index 33a634e9b..8efdcb472 100644 --- a/web/components/modules/select/status.tsx +++ b/web/components/modules/select/status.tsx @@ -5,9 +5,9 @@ import { Controller, FieldError, Control } from "react-hook-form"; // ui import { CustomSelect, DoubleCircleIcon, ModuleStatusIcon } from "@plane/ui"; // types +import { MODULE_STATUS } from "constants/module"; import type { IModule } from "@plane/types"; // constants -import { MODULE_STATUS } from "constants/module"; type Props = { control: Control; diff --git a/web/components/modules/sidebar-select/select-status.tsx b/web/components/modules/sidebar-select/select-status.tsx index b8c337fd4..4a203ee62 100644 --- a/web/components/modules/sidebar-select/select-status.tsx +++ b/web/components/modules/sidebar-select/select-status.tsx @@ -5,10 +5,10 @@ import { Control, Controller, UseFormWatch } from "react-hook-form"; // ui import { CustomSelect, DoubleCircleIcon } from "@plane/ui"; // types +import { MODULE_STATUS } from "constants/module"; import { IModule } from "@plane/types"; // common // constants -import { MODULE_STATUS } from "constants/module"; type Props = { control: Control, any>; diff --git a/web/components/modules/sidebar.tsx b/web/components/modules/sidebar.tsx index ad3da373c..c9f28cf98 100644 --- a/web/components/modules/sidebar.tsx +++ b/web/components/modules/sidebar.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; import { Disclosure, Transition } from "@headlessui/react"; import { @@ -14,13 +14,6 @@ import { Trash2, UserCircle2, } from "lucide-react"; -// hooks -import { useModule, useUser, useEventTracker } from "hooks/store"; -// components -import { LinkModal, LinksList, SidebarProgressStats } from "components/core"; -import { DeleteModuleModal } from "components/modules"; -import ProgressChart from "components/core/sidebar/progress-chart"; -import { DateRangeDropdown, MemberDropdown } from "components/dropdowns"; // ui import { CustomMenu, @@ -32,15 +25,22 @@ import { TOAST_TYPE, setToast, } from "@plane/ui"; +// components +import { LinkModal, LinksList, SidebarProgressStats } from "components/core"; +import ProgressChart from "components/core/sidebar/progress-chart"; +import { DateRangeDropdown, MemberDropdown } from "components/dropdowns"; +import { DeleteModuleModal } from "components/modules"; +// constant +import { MODULE_LINK_CREATED, MODULE_LINK_DELETED, MODULE_LINK_UPDATED, MODULE_UPDATED } from "constants/event-tracker"; +import { MODULE_STATUS } from "constants/module"; +import { EUserProjectRoles } from "constants/project"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { copyUrlToClipboard } from "helpers/string.helper"; +// hooks +import { useModule, useUser, useEventTracker } from "hooks/store"; // types import { ILinkDetails, IModule, ModuleLink } from "@plane/types"; -// constant -import { MODULE_STATUS } from "constants/module"; -import { EUserProjectRoles } from "constants/project"; -import { MODULE_LINK_CREATED, MODULE_LINK_DELETED, MODULE_LINK_UPDATED, MODULE_UPDATED } from "constants/event-tracker"; const defaultValues: Partial = { lead_id: "", @@ -340,7 +340,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { Date range
    -
    +
    = observer((props) => { control={control} name="lead_id" render={({ field: { value } }) => ( -
    +
    { @@ -408,7 +408,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { control={control} name="member_ids" render={({ field: { value } }) => ( -
    +
    { @@ -429,7 +429,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { Issues
    -
    +
    {issueCount}
    diff --git a/web/components/notifications/notification-card.tsx b/web/components/notifications/notification-card.tsx index bd26dcfa5..03f75ca63 100644 --- a/web/components/notifications/notification-card.tsx +++ b/web/components/notifications/notification-card.tsx @@ -1,22 +1,22 @@ import React, { useEffect, useRef } from "react"; import Image from "next/image"; -import { useRouter } from "next/router"; import Link from "next/link"; +import { useRouter } from "next/router"; import { Menu } from "@headlessui/react"; -import { ArchiveRestore, Clock, MessageSquare, MoreVertical, User2 } from "lucide-react"; -// hooks -import { useEventTracker } from "hooks/store"; // icons +import { ArchiveRestore, Clock, MessageSquare, MoreVertical, User2 } from "lucide-react"; +// ui import { ArchiveIcon, CustomMenu, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // constants +import { ISSUE_OPENED, NOTIFICATIONS_READ, NOTIFICATION_ARCHIVED, NOTIFICATION_SNOOZED } from "constants/event-tracker"; import { snoozeOptions } from "constants/notification"; // helper -import { replaceUnderscoreIfSnakeCase, truncateText, stripAndTruncateHTML } from "helpers/string.helper"; import { calculateTimeAgo, renderFormattedTime, renderFormattedDate } from "helpers/date-time.helper"; +import { replaceUnderscoreIfSnakeCase, truncateText, stripAndTruncateHTML } from "helpers/string.helper"; +// hooks +import { useEventTracker } from "hooks/store"; // type import type { IUserNotification, NotificationType } from "@plane/types"; -// constants -import { ISSUE_OPENED, NOTIFICATIONS_READ, NOTIFICATION_ARCHIVED, NOTIFICATION_SNOOZED } from "constants/event-tracker"; type NotificationCardProps = { selectedTab: NotificationType; @@ -215,7 +215,7 @@ export const NotificationCard: React.FC = (props) => { {notification.message}
    )} -
    +
    {({ open }) => ( <> @@ -231,11 +231,11 @@ export const NotificationCard: React.FC = (props) => {
    {moreOptions.map((item) => ( - + {({ close }) => (
    @@ -357,7 +358,7 @@ export const NotificationCard: React.FC = (props) => { }, }, ].map((item) => ( - +
diff --git a/web/components/notifications/notification-popover.tsx b/web/components/notifications/notification-popover.tsx index a8f25762e..d7aa1b07d 100644 --- a/web/components/notifications/notification-popover.tsx +++ b/web/components/notifications/notification-popover.tsx @@ -1,20 +1,20 @@ import React, { Fragment } from "react"; +import { observer } from "mobx-react-lite"; import { Popover, Transition } from "@headlessui/react"; import { Bell } from "lucide-react"; -import { observer } from "mobx-react-lite"; // hooks -import { useApplication } from "hooks/store"; -import useUserNotification from "hooks/use-user-notifications"; -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -// components +import { Tooltip } from "@plane/ui"; import { EmptyState } from "components/common"; import { SnoozeNotificationModal, NotificationCard, NotificationHeader } from "components/notifications"; -import { Tooltip } from "@plane/ui"; import { NotificationsLoader } from "components/ui"; +import { getNumberCount } from "helpers/string.helper"; +import { useApplication } from "hooks/store"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; +import useUserNotification from "hooks/use-user-notifications"; +// components // images import emptyNotification from "public/empty-state/notification.svg"; // helpers -import { getNumberCount } from "helpers/string.helper"; export const NotificationPopover = observer(() => { // states diff --git a/web/components/notifications/select-snooze-till-modal.tsx b/web/components/notifications/select-snooze-till-modal.tsx index c2875b8dd..f65d51ba7 100644 --- a/web/components/notifications/select-snooze-till-modal.tsx +++ b/web/components/notifications/select-snooze-till-modal.tsx @@ -1,13 +1,13 @@ import { Fragment, FC } from "react"; import { useRouter } from "next/router"; import { useForm, Controller } from "react-hook-form"; -import { DateDropdown } from "components/dropdowns"; import { Transition, Dialog } from "@headlessui/react"; import { X } from "lucide-react"; +import { Button, CustomSelect, TOAST_TYPE, setToast } from "@plane/ui"; +import { DateDropdown } from "components/dropdowns"; // constants import { allTimeIn30MinutesInterval12HoursFormat } from "constants/notification"; // ui -import { Button, CustomSelect, TOAST_TYPE, setToast } from "@plane/ui"; // types import type { IUserNotification } from "@plane/types"; @@ -143,7 +143,7 @@ export const SnoozeNotificationModal: FC = (props) => { leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > - +
@@ -157,7 +157,7 @@ export const SnoozeNotificationModal: FC = (props) => {
-
+
Pick a date
= (props) => { onClick={() => { setValue("period", "AM"); }} - className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${watch("period") === "AM" + className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${ + watch("period") === "AM" ? "bg-custom-primary-100/90 text-custom-primary-0" : "bg-custom-background-80" - }`} + }`} > AM
@@ -221,10 +222,11 @@ export const SnoozeNotificationModal: FC = (props) => { onClick={() => { setValue("period", "PM"); }} - className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${watch("period") === "PM" + className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${ + watch("period") === "PM" ? "bg-custom-primary-100/90 text-custom-primary-0" : "bg-custom-background-80" - }`} + }`} > PM
diff --git a/web/components/onboarding/invitations.tsx b/web/components/onboarding/invitations.tsx index c176ed580..2e94bb67e 100644 --- a/web/components/onboarding/invitations.tsx +++ b/web/components/onboarding/invitations.tsx @@ -1,23 +1,23 @@ import React, { useState } from "react"; import useSWR, { mutate } from "swr"; // hooks +import { CheckCircle2, Search } from "lucide-react"; +import { Button } from "@plane/ui"; +import { MEMBER_ACCEPTED } from "constants/event-tracker"; +import { USER_WORKSPACES, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys"; +import { ROLE } from "constants/workspace"; +import { truncateText } from "helpers/string.helper"; +import { getUserRole } from "helpers/user.helper"; import { useEventTracker, useUser, useWorkspace } from "hooks/store"; // components -import { Button } from "@plane/ui"; // helpers -import { truncateText } from "helpers/string.helper"; // services import { WorkspaceService } from "services/workspace.service"; // constants -import { USER_WORKSPACES, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys"; -import { ROLE } from "constants/workspace"; -import { MEMBER_ACCEPTED } from "constants/event-tracker"; // types import { IWorkspaceMemberInvitation } from "@plane/types"; // icons -import { CheckCircle2, Search } from "lucide-react"; import {} from "hooks/store/use-event-tracker"; -import { getUserRole } from "helpers/user.helper"; type Props = { handleNextStep: () => void; @@ -57,17 +57,18 @@ export const Invitations: React.FC = (props) => { }; const submitInvitations = async () => { - if (invitationsRespond.length <= 0) return; + const invitation = invitations?.find((invitation) => invitation.id === invitationsRespond[0]); + + if (invitationsRespond.length <= 0 && !invitation?.role) return; setIsJoiningWorkspaces(true); - const invitation = invitations?.find((invitation) => invitation.id === invitationsRespond[0]); await workspaceService .joinWorkspaces({ invitations: invitationsRespond }) .then(async () => { captureEvent(MEMBER_ACCEPTED, { member_id: invitation?.id, - role: getUserRole(invitation?.role!), + role: getUserRole(invitation?.role as any), project_id: undefined, accepted_from: "App", state: "SUCCESS", @@ -83,7 +84,7 @@ export const Invitations: React.FC = (props) => { console.error(error); captureEvent(MEMBER_ACCEPTED, { member_id: invitation?.id, - role: getUserRole(invitation?.role!), + role: getUserRole(invitation?.role as any), project_id: undefined, accepted_from: "App", state: "FAILED", diff --git a/web/components/onboarding/invite-members.tsx b/web/components/onboarding/invite-members.tsx index 1f78fcf20..c5a0d51c2 100644 --- a/web/components/onboarding/invite-members.tsx +++ b/web/components/onboarding/invite-members.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useRef, useState } from "react"; import Image from "next/image"; import { useTheme } from "next-themes"; -import { Listbox, Transition } from "@headlessui/react"; import { Control, Controller, @@ -13,29 +12,30 @@ import { useFieldArray, useForm, } from "react-hook-form"; +import { Listbox, Transition } from "@headlessui/react"; +// icons import { Check, ChevronDown, Plus, XCircle } from "lucide-react"; -// services -import { WorkspaceService } from "services/workspace.service"; -// hooks -import { useEventTracker } from "hooks/store"; // ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // components import { OnboardingStepIndicator } from "components/onboarding/step-indicator"; -// hooks -import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; -// types -import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types"; // constants -import { EUserWorkspaceRoles, ROLE } from "constants/workspace"; import { MEMBER_INVITED } from "constants/event-tracker"; +import { EUserWorkspaceRoles, ROLE } from "constants/workspace"; // helpers import { getUserRole } from "helpers/user.helper"; +// hooks +import { useEventTracker } from "hooks/store"; +import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; // assets -import user1 from "public/users/user-1.png"; -import user2 from "public/users/user-2.png"; import userDark from "public/onboarding/user-dark.svg"; import userLight from "public/onboarding/user-light.svg"; +import user1 from "public/users/user-1.png"; +import user2 from "public/users/user-2.png"; +// services +import { WorkspaceService } from "services/workspace.service"; +// types +import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types"; type Props = { finishOnboarding: () => Promise; @@ -368,8 +368,8 @@ export const InviteMembers: React.FC = (props) => { >

Members

- {Array.from({ length: 4 }).map(() => ( -
+ {Array.from({ length: 4 }).map((i) => ( +
user
diff --git a/web/components/onboarding/join-workspaces.tsx b/web/components/onboarding/join-workspaces.tsx index 08ffab379..e59db31c7 100644 --- a/web/components/onboarding/join-workspaces.tsx +++ b/web/components/onboarding/join-workspaces.tsx @@ -1,10 +1,10 @@ import React from "react"; -import { Controller, useForm } from "react-hook-form"; import { observer } from "mobx-react-lite"; +import { Controller, useForm } from "react-hook-form"; // hooks +import { Invitations, OnboardingSidebar, OnboardingStepIndicator, Workspace } from "components/onboarding"; import { useUser } from "hooks/store"; // components -import { Invitations, OnboardingSidebar, OnboardingStepIndicator, Workspace } from "components/onboarding"; // types import { IWorkspace, TOnboardingSteps } from "@plane/types"; diff --git a/web/components/onboarding/onboarding-sidebar.tsx b/web/components/onboarding/onboarding-sidebar.tsx index af0da75ca..42ec102cb 100644 --- a/web/components/onboarding/onboarding-sidebar.tsx +++ b/web/components/onboarding/onboarding-sidebar.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from "react"; -import { useTheme } from "next-themes"; import Image from "next/image"; +import { useTheme } from "next-themes"; import { Control, Controller, UseFormSetValue, UseFormWatch } from "react-hook-form"; import { BarChart2, @@ -20,9 +20,9 @@ import { Avatar, DiceIcon, PhotoFilterIcon } from "@plane/ui"; // hooks import { useUser, useWorkspace } from "hooks/store"; // types +import projectEmoji from "public/emoji/project-emoji.svg"; import { IWorkspace } from "@plane/types"; // assets -import projectEmoji from "public/emoji/project-emoji.svg"; const workspaceLinks = [ { @@ -86,8 +86,9 @@ type Props = { watch?: UseFormWatch; userFullName?: string; }; -var timer: number = 0; -var lastWorkspaceName: string = ""; + +let timer: number = 0; +let lastWorkspaceName: string = ""; export const OnboardingSidebar: React.FC = (props) => { const { workspaceName, showProject, control, setValue, watch, userFullName } = props; diff --git a/web/components/onboarding/switch-delete-account-modal.tsx b/web/components/onboarding/switch-delete-account-modal.tsx index ff37e5802..c84911220 100644 --- a/web/components/onboarding/switch-delete-account-modal.tsx +++ b/web/components/onboarding/switch-delete-account-modal.tsx @@ -1,13 +1,13 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; -import { mutate } from "swr"; import { useTheme } from "next-themes"; +import { mutate } from "swr"; import { Dialog, Transition } from "@headlessui/react"; import { Trash2 } from "lucide-react"; // hooks +import { TOAST_TYPE, setToast } from "@plane/ui"; import { useUser } from "hooks/store"; // ui -import { TOAST_TYPE, setToast } from "@plane/ui"; type Props = { isOpen: boolean; diff --git a/web/components/onboarding/tour/root.tsx b/web/components/onboarding/tour/root.tsx index c09a2a94c..4c44f8c62 100644 --- a/web/components/onboarding/tour/root.tsx +++ b/web/components/onboarding/tour/root.tsx @@ -1,22 +1,22 @@ import { useState } from "react"; -import Image from "next/image"; import { observer } from "mobx-react-lite"; +import Image from "next/image"; import { X } from "lucide-react"; // hooks +import { Button } from "@plane/ui"; +import { TourSidebar } from "components/onboarding"; +import { PRODUCT_TOUR_SKIPPED, PRODUCT_TOUR_STARTED } from "constants/event-tracker"; import { useApplication, useEventTracker, useUser } from "hooks/store"; // components -import { TourSidebar } from "components/onboarding"; // ui -import { Button } from "@plane/ui"; // assets -import PlaneWhiteLogo from "public/plane-logos/white-horizontal.svg"; -import IssuesTour from "public/onboarding/issues.webp"; import CyclesTour from "public/onboarding/cycles.webp"; +import IssuesTour from "public/onboarding/issues.webp"; import ModulesTour from "public/onboarding/modules.webp"; -import ViewsTour from "public/onboarding/views.webp"; import PagesTour from "public/onboarding/pages.webp"; +import ViewsTour from "public/onboarding/views.webp"; +import PlaneWhiteLogo from "public/plane-logos/white-horizontal.svg"; // constants -import { PRODUCT_TOUR_SKIPPED, PRODUCT_TOUR_STARTED } from "constants/event-tracker"; type Props = { onComplete: () => void; diff --git a/web/components/onboarding/tour/sidebar.tsx b/web/components/onboarding/tour/sidebar.tsx index 350bd638a..535002493 100644 --- a/web/components/onboarding/tour/sidebar.tsx +++ b/web/components/onboarding/tour/sidebar.tsx @@ -1,6 +1,6 @@ // icons -import { ContrastIcon, DiceIcon, LayersIcon, PhotoFilterIcon } from "@plane/ui"; import { FileText } from "lucide-react"; +import { ContrastIcon, DiceIcon, LayersIcon, PhotoFilterIcon } from "@plane/ui"; // types import { TTourSteps } from "./root"; diff --git a/web/components/onboarding/user-details.tsx b/web/components/onboarding/user-details.tsx index a29df3c94..820f08da6 100644 --- a/web/components/onboarding/user-details.tsx +++ b/web/components/onboarding/user-details.tsx @@ -1,21 +1,22 @@ import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; import Image from "next/image"; import { Controller, useForm } from "react-hook-form"; -import { observer } from "mobx-react-lite"; import { Camera, User2 } from "lucide-react"; +import { Button, Input } from "@plane/ui"; +// components +import { UserImageUploadModal } from "components/core"; +import { OnboardingSidebar, OnboardingStepIndicator } from "components/onboarding"; +// constants +import { USER_DETAILS } from "constants/event-tracker"; // hooks import { useEventTracker, useUser, useWorkspace } from "hooks/store"; -// components -import { Button, Input } from "@plane/ui"; -import { OnboardingSidebar, OnboardingStepIndicator } from "components/onboarding"; -import { UserImageUploadModal } from "components/core"; -// types -import { IUser } from "@plane/types"; -// services -import { FileService } from "services/file.service"; // assets import IssuesSvg from "public/onboarding/onboarding-issues.webp"; -import { USER_DETAILS } from "constants/event-tracker"; +// services +import { FileService } from "services/file.service"; +// types +import { IUser } from "@plane/types"; const defaultValues: Partial = { first_name: "", @@ -183,7 +184,7 @@ export const UserDetails: React.FC = observer((props) => { name="first_name" type="text" value={value} - autoFocus={true} + autoFocus onChange={(event) => { setUserName(event.target.value); onChange(event); @@ -220,6 +221,7 @@ export const UserDetails: React.FC = observer((props) => {
{USE_CASES.map((useCase) => (
) => Promise; diff --git a/web/components/page-views/signin.tsx b/web/components/page-views/signin.tsx index 2f1d62f84..7929a5e37 100644 --- a/web/components/page-views/signin.tsx +++ b/web/components/page-views/signin.tsx @@ -2,13 +2,13 @@ import { useEffect } from "react"; import { observer } from "mobx-react-lite"; import Image from "next/image"; // hooks +import { Spinner } from "@plane/ui"; +import { SignInRoot } from "components/account"; +import { PageHead } from "components/core"; import { useApplication, useUser } from "hooks/store"; import useSignInRedirection from "hooks/use-sign-in-redirection"; // components -import { SignInRoot } from "components/account"; -import { PageHead } from "components/core"; // ui -import { Spinner } from "@plane/ui"; // images import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; diff --git a/web/components/page-views/workspace-dashboard.tsx b/web/components/page-views/workspace-dashboard.tsx index 0d5d4115a..2f8392bc2 100644 --- a/web/components/page-views/workspace-dashboard.tsx +++ b/web/components/page-views/workspace-dashboard.tsx @@ -1,20 +1,20 @@ import { useEffect } from "react"; -import { useTheme } from "next-themes"; import { observer } from "mobx-react-lite"; +import { useTheme } from "next-themes"; // hooks -import { useApplication, useEventTracker, useDashboard, useProject, useUser } from "hooks/store"; // components -import { TourRoot } from "components/onboarding"; -import { UserGreetingsView } from "components/user"; -import { IssuePeekOverview } from "components/issues"; +import { Spinner } from "@plane/ui"; import { DashboardWidgets } from "components/dashboard"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { IssuePeekOverview } from "components/issues"; +import { TourRoot } from "components/onboarding"; +import { UserGreetingsView } from "components/user"; // ui -import { Spinner } from "@plane/ui"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; -import { PRODUCT_TOUR_COMPLETED } from "constants/event-tracker"; import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { PRODUCT_TOUR_COMPLETED } from "constants/event-tracker"; +import { EUserWorkspaceRoles } from "constants/workspace"; +import { useApplication, useEventTracker, useDashboard, useProject, useUser } from "hooks/store"; export const WorkspaceDashboardView = observer(() => { // theme diff --git a/web/components/pages/create-update-page-modal.tsx b/web/components/pages/create-update-page-modal.tsx index eea7e9d7f..c3e22e52e 100644 --- a/web/components/pages/create-update-page-modal.tsx +++ b/web/components/pages/create-update-page-modal.tsx @@ -2,15 +2,15 @@ import React, { FC } from "react"; import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; // components -import { PageForm } from "./page-form"; -// hooks +import { PAGE_CREATED, PAGE_UPDATED } from "constants/event-tracker"; import { useEventTracker } from "hooks/store"; +// hooks // types -import { IPage } from "@plane/types"; import { useProjectPages } from "hooks/store/use-project-page"; import { IPageStore } from "store/page.store"; +import { IPage } from "@plane/types"; +import { PageForm } from "./page-form"; // constants -import { PAGE_CREATED, PAGE_UPDATED } from "constants/event-tracker"; type Props = { // data?: IPage | null; diff --git a/web/components/pages/delete-page-modal.tsx b/web/components/pages/delete-page-modal.tsx index 67cd175f0..362dae172 100644 --- a/web/components/pages/delete-page-modal.tsx +++ b/web/components/pages/delete-page-modal.tsx @@ -1,16 +1,16 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; -// hooks -import { useEventTracker, usePage } from "hooks/store"; // ui import { Button, TOAST_TYPE, setToast } from "@plane/ui"; -// types -import { useProjectPages } from "hooks/store/use-project-page"; // constants import { PAGE_DELETED } from "constants/event-tracker"; +// hooks +import { useEventTracker, usePage } from "hooks/store"; +import { useProjectPages } from "hooks/store/use-project-page"; +// types type TConfirmPageDeletionProps = { pageId: string; diff --git a/web/components/pages/page-form.tsx b/web/components/pages/page-form.tsx index 4f5874e5f..97e881096 100644 --- a/web/components/pages/page-form.tsx +++ b/web/components/pages/page-form.tsx @@ -2,10 +2,10 @@ import { Controller, useForm } from "react-hook-form"; // ui import { Button, Input, Tooltip } from "@plane/ui"; // types -import { IPage } from "@plane/types"; // constants import { PAGE_ACCESS_SPECIFIERS } from "constants/page"; import { IPageStore } from "store/page.store"; +import { IPage } from "@plane/types"; type Props = { handleFormSubmit: (values: IPage) => Promise; diff --git a/web/components/pages/pages-list/all-pages-list.tsx b/web/components/pages/pages-list/all-pages-list.tsx index 4ed759a0f..e7cb21775 100644 --- a/web/components/pages/pages-list/all-pages-list.tsx +++ b/web/components/pages/pages-list/all-pages-list.tsx @@ -1,9 +1,9 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // hooks +import { Loader } from "@plane/ui"; import { PagesListView } from "components/pages/pages-list"; // ui -import { Loader } from "@plane/ui"; import { useProjectPages } from "hooks/store/use-project-specific-pages"; export const AllPagesList: FC = observer(() => { diff --git a/web/components/pages/pages-list/archived-pages-list.tsx b/web/components/pages/pages-list/archived-pages-list.tsx index eb57d7558..f7bcb6059 100644 --- a/web/components/pages/pages-list/archived-pages-list.tsx +++ b/web/components/pages/pages-list/archived-pages-list.tsx @@ -1,10 +1,10 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // components +import { Loader, Spinner } from "@plane/ui"; import { PagesListView } from "components/pages/pages-list"; // hooks // ui -import { Loader, Spinner } from "@plane/ui"; import { useProjectPages } from "hooks/store/use-project-specific-pages"; export const ArchivedPagesList: FC = observer(() => { diff --git a/web/components/pages/pages-list/favorite-pages-list.tsx b/web/components/pages/pages-list/favorite-pages-list.tsx index 4ce301a68..4d2ad5019 100644 --- a/web/components/pages/pages-list/favorite-pages-list.tsx +++ b/web/components/pages/pages-list/favorite-pages-list.tsx @@ -1,10 +1,10 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // components +import { Loader } from "@plane/ui"; import { PagesListView } from "components/pages/pages-list"; // hooks // ui -import { Loader } from "@plane/ui"; import { useProjectPages } from "hooks/store/use-project-specific-pages"; export const FavoritePagesList: FC = observer(() => { diff --git a/web/components/pages/pages-list/list-item.tsx b/web/components/pages/pages-list/list-item.tsx index 6b1a4793d..d4cb3c023 100644 --- a/web/components/pages/pages-list/list-item.tsx +++ b/web/components/pages/pages-list/list-item.tsx @@ -1,6 +1,7 @@ import { FC, useState } from "react"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; +import { useRouter } from "next/router"; import { AlertCircle, Archive, @@ -13,17 +14,16 @@ import { Star, Trash2, } from "lucide-react"; -import { copyUrlToClipboard } from "helpers/string.helper"; -import { renderFormattedTime, renderFormattedDate } from "helpers/date-time.helper"; // ui import { CustomMenu, Tooltip } from "@plane/ui"; // components import { CreateUpdatePageModal, DeletePageModal } from "components/pages"; // constants import { EUserProjectRoles } from "constants/project"; -import { useRouter } from "next/router"; -import { useProjectPages } from "hooks/store/use-project-specific-pages"; +import { renderFormattedTime, renderFormattedDate } from "helpers/date-time.helper"; +import { copyUrlToClipboard } from "helpers/string.helper"; import { useMember, usePage, useUser } from "hooks/store"; +import { useProjectPages } from "hooks/store/use-project-specific-pages"; import { IIssueLabel } from "@plane/types"; export interface IPagesListItem { diff --git a/web/components/pages/pages-list/list-view.tsx b/web/components/pages/pages-list/list-view.tsx index ebd1fa128..0d468ef3c 100644 --- a/web/components/pages/pages-list/list-view.tsx +++ b/web/components/pages/pages-list/list-view.tsx @@ -2,16 +2,16 @@ import { FC } from "react"; import { useRouter } from "next/router"; import { useTheme } from "next-themes"; // hooks +import { Loader } from "@plane/ui"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EUserProjectRoles } from "constants/project"; import { useApplication, useUser } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // components -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { PagesListItem } from "./list-item"; // ui -import { Loader } from "@plane/ui"; // constants -import { EUserProjectRoles } from "constants/project"; -import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state"; type IPagesListView = { pageIds: string[]; diff --git a/web/components/pages/pages-list/private-page-list.tsx b/web/components/pages/pages-list/private-page-list.tsx index 15a577d80..316c6bf44 100644 --- a/web/components/pages/pages-list/private-page-list.tsx +++ b/web/components/pages/pages-list/private-page-list.tsx @@ -2,9 +2,9 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // hooks // components +import { Loader } from "@plane/ui"; import { PagesListView } from "components/pages/pages-list"; // ui -import { Loader } from "@plane/ui"; import { useProjectPages } from "hooks/store/use-project-specific-pages"; export const PrivatePagesList: FC = observer(() => { diff --git a/web/components/pages/pages-list/recent-pages-list.tsx b/web/components/pages/pages-list/recent-pages-list.tsx index 71bbf12ac..28a430031 100644 --- a/web/components/pages/pages-list/recent-pages-list.tsx +++ b/web/components/pages/pages-list/recent-pages-list.tsx @@ -2,18 +2,18 @@ import React, { FC } from "react"; import { observer } from "mobx-react-lite"; import { useTheme } from "next-themes"; // hooks +import { Loader } from "@plane/ui"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { PagesListView } from "components/pages/pages-list"; +import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EUserProjectRoles } from "constants/project"; +import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; import { useApplication, useUser } from "hooks/store"; import { useProjectPages } from "hooks/store/use-project-specific-pages"; // components -import { PagesListView } from "components/pages/pages-list"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui -import { Loader } from "@plane/ui"; // helpers -import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; // constants -import { EUserProjectRoles } from "constants/project"; -import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state"; export const RecentPagesList: FC = observer(() => { // theme diff --git a/web/components/pages/pages-list/shared-pages-list.tsx b/web/components/pages/pages-list/shared-pages-list.tsx index d20a1350e..2626db13c 100644 --- a/web/components/pages/pages-list/shared-pages-list.tsx +++ b/web/components/pages/pages-list/shared-pages-list.tsx @@ -1,10 +1,10 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // components +import { Loader } from "@plane/ui"; import { PagesListView } from "components/pages/pages-list"; // hooks // ui -import { Loader } from "@plane/ui"; import { useProjectPages } from "hooks/store/use-project-specific-pages"; export const SharedPagesList: FC = observer(() => { diff --git a/web/components/profile/activity/activity-list.tsx b/web/components/profile/activity/activity-list.tsx index 066912721..e77128e9f 100644 --- a/web/components/profile/activity/activity-list.tsx +++ b/web/components/profile/activity/activity-list.tsx @@ -1,16 +1,16 @@ -import Link from "next/link"; +import { RichReadOnlyEditor } from "@plane/rich-text-editor"; import { observer } from "mobx-react"; +import Link from "next/link"; import { History, MessageSquare } from "lucide-react"; // editor -import { RichReadOnlyEditor } from "@plane/rich-text-editor"; // hooks -import { useUser } from "hooks/store"; // components import { ActivityIcon, ActivityMessage, IssueLink } from "components/core"; // ui import { ActivitySettingsLoader } from "components/ui"; // helpers import { calculateTimeAgo } from "helpers/date-time.helper"; +import { useUser } from "hooks/store"; // types import { IUserActivityResponse } from "@plane/types"; diff --git a/web/components/profile/activity/download-button.tsx b/web/components/profile/activity/download-button.tsx index ff928dc2a..491ebf45f 100644 --- a/web/components/profile/activity/download-button.tsx +++ b/web/components/profile/activity/download-button.tsx @@ -1,11 +1,11 @@ import { useState } from "react"; import { useRouter } from "next/router"; // services -import { UserService } from "services/user.service"; // ui import { Button } from "@plane/ui"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { UserService } from "services/user.service"; const userService = new UserService(); diff --git a/web/components/profile/activity/profile-activity-list.tsx b/web/components/profile/activity/profile-activity-list.tsx index 3912c8568..6311c382c 100644 --- a/web/components/profile/activity/profile-activity-list.tsx +++ b/web/components/profile/activity/profile-activity-list.tsx @@ -1,22 +1,22 @@ import { useEffect } from "react"; -import Link from "next/link"; +import { RichReadOnlyEditor } from "@plane/rich-text-editor"; import { observer } from "mobx-react"; +import Link from "next/link"; import useSWR from "swr"; import { History, MessageSquare } from "lucide-react"; // hooks +import { ActivityIcon, ActivityMessage, IssueLink } from "components/core"; +import { ActivitySettingsLoader } from "components/ui"; +import { USER_ACTIVITY } from "constants/fetch-keys"; +import { calculateTimeAgo } from "helpers/date-time.helper"; import { useUser } from "hooks/store"; // services import { UserService } from "services/user.service"; // editor -import { RichReadOnlyEditor } from "@plane/rich-text-editor"; // components -import { ActivityIcon, ActivityMessage, IssueLink } from "components/core"; // ui -import { ActivitySettingsLoader } from "components/ui"; // helpers -import { calculateTimeAgo } from "helpers/date-time.helper"; // fetch-keys -import { USER_ACTIVITY } from "constants/fetch-keys"; // services const userService = new UserService(); diff --git a/web/components/profile/activity/workspace-activity-list.tsx b/web/components/profile/activity/workspace-activity-list.tsx index c2c75a195..aa5a03dee 100644 --- a/web/components/profile/activity/workspace-activity-list.tsx +++ b/web/components/profile/activity/workspace-activity-list.tsx @@ -2,11 +2,11 @@ import { useEffect } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; // services +import { USER_PROFILE_ACTIVITY } from "constants/fetch-keys"; import { UserService } from "services/user.service"; // components import { ActivityList } from "./activity-list"; // fetch-keys -import { USER_PROFILE_ACTIVITY } from "constants/fetch-keys"; // services const userService = new UserService(); diff --git a/web/components/profile/navbar.tsx b/web/components/profile/navbar.tsx index 582f0f26b..ecc0028db 100644 --- a/web/components/profile/navbar.tsx +++ b/web/components/profile/navbar.tsx @@ -1,7 +1,7 @@ import React from "react"; -import { useRouter } from "next/router"; import Link from "next/link"; +import { useRouter } from "next/router"; // components import { ProfileIssuesFilter } from "components/profile"; diff --git a/web/components/profile/overview/activity.tsx b/web/components/profile/overview/activity.tsx index 112c073ab..4a6cf98be 100644 --- a/web/components/profile/overview/activity.tsx +++ b/web/components/profile/overview/activity.tsx @@ -1,21 +1,21 @@ +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react"; //hooks +import { Loader } from "@plane/ui"; +import { ActivityMessage, IssueLink } from "components/core"; +import { ProfileEmptyState } from "components/ui"; +import { USER_PROFILE_ACTIVITY } from "constants/fetch-keys"; +import { calculateTimeAgo } from "helpers/date-time.helper"; import { useUser } from "hooks/store"; // services +import recentActivityEmptyState from "public/empty-state/recent_activity.svg"; import { UserService } from "services/user.service"; // components -import { ActivityMessage, IssueLink } from "components/core"; // ui -import { ProfileEmptyState } from "components/ui"; -import { Loader } from "@plane/ui"; // image -import recentActivityEmptyState from "public/empty-state/recent_activity.svg"; // helpers -import { calculateTimeAgo } from "helpers/date-time.helper"; // fetch-keys -import { USER_PROFILE_ACTIVITY } from "constants/fetch-keys"; // services const userService = new UserService(); diff --git a/web/components/profile/overview/priority-distribution.tsx b/web/components/profile/overview/priority-distribution.tsx new file mode 100644 index 000000000..12e430409 --- /dev/null +++ b/web/components/profile/overview/priority-distribution.tsx @@ -0,0 +1,88 @@ +// ui +import { Loader } from "@plane/ui"; +import { BarGraph, ProfileEmptyState } from "components/ui"; +// image +import { capitalizeFirstLetter } from "helpers/string.helper"; +import emptyBarGraph from "public/empty-state/empty_bar_graph.svg"; +// helpers +// types +import { IUserProfileData } from "@plane/types"; + +type Props = { + userProfile: IUserProfileData | undefined; +}; + +export const ProfilePriorityDistribution: React.FC = ({ userProfile }) => ( +
+

Issues by Priority

+ {userProfile ? ( +
+ {userProfile.priority_distribution.length > 0 ? ( + ({ + priority: capitalizeFirstLetter(priority.priority ?? "None"), + value: priority.priority_count, + }))} + height="300px" + indexBy="priority" + keys={["value"]} + borderRadius={4} + padding={0.7} + customYAxisTickValues={userProfile.priority_distribution.map((p) => p.priority_count)} + tooltip={(datum) => ( +
+ + {datum.data.priority}: + {datum.value} +
+ )} + colors={(datum) => { + if (datum.data.priority === "Urgent") return "#991b1b"; + else if (datum.data.priority === "High") return "#ef4444"; + else if (datum.data.priority === "Medium") return "#f59e0b"; + else if (datum.data.priority === "Low") return "#16a34a"; + else return "#e5e5e5"; + }} + theme={{ + axis: { + domain: { + line: { + stroke: "transparent", + }, + }, + }, + grid: { + line: { + stroke: "transparent", + }, + }, + }} + /> + ) : ( +
+ +
+ )} +
+ ) : ( +
+ + + + + + + +
+ )} +
+); diff --git a/web/components/profile/overview/priority-distribution/priority-distribution.tsx b/web/components/profile/overview/priority-distribution/priority-distribution.tsx index 63559bdee..48d612dff 100644 --- a/web/components/profile/overview/priority-distribution/priority-distribution.tsx +++ b/web/components/profile/overview/priority-distribution/priority-distribution.tsx @@ -1,9 +1,9 @@ // components -import { PriorityDistributionContent } from "./main-content"; // ui import { Loader } from "@plane/ui"; // types import { IUserPriorityDistribution } from "@plane/types"; +import { PriorityDistributionContent } from "./main-content"; type Props = { priorityDistribution: IUserPriorityDistribution[] | undefined; diff --git a/web/components/profile/overview/state-distribution.tsx b/web/components/profile/overview/state-distribution.tsx index f38283aa7..25de06c84 100644 --- a/web/components/profile/overview/state-distribution.tsx +++ b/web/components/profile/overview/state-distribution.tsx @@ -1,11 +1,11 @@ // ui import { ProfileEmptyState, PieGraph } from "components/ui"; // image +import { STATE_GROUPS } from "constants/state"; import stateGraph from "public/empty-state/state_graph.svg"; // types import { IUserProfileData, IUserStateDistribution } from "@plane/types"; // constants -import { STATE_GROUPS } from "constants/state"; type Props = { stateDistribution: IUserStateDistribution[]; diff --git a/web/components/profile/overview/stats.tsx b/web/components/profile/overview/stats.tsx index 3f96488a0..62873ee38 100644 --- a/web/components/profile/overview/stats.tsx +++ b/web/components/profile/overview/stats.tsx @@ -1,9 +1,9 @@ -import { useRouter } from "next/router"; import Link from "next/link"; +import { useRouter } from "next/router"; // ui -import { CreateIcon, LayerStackIcon, Loader } from "@plane/ui"; import { UserCircle2 } from "lucide-react"; +import { CreateIcon, LayerStackIcon, Loader } from "@plane/ui"; // types import { IUserProfileData } from "@plane/types"; diff --git a/web/components/profile/overview/workload.tsx b/web/components/profile/overview/workload.tsx index 86989748d..54e03b047 100644 --- a/web/components/profile/overview/workload.tsx +++ b/web/components/profile/overview/workload.tsx @@ -1,7 +1,7 @@ // types +import { STATE_GROUPS } from "constants/state"; import { IUserStateDistribution } from "@plane/types"; // constants -import { STATE_GROUPS } from "constants/state"; type Props = { stateDistribution: IUserStateDistribution[]; diff --git a/web/components/profile/preferences/index.ts b/web/components/profile/preferences/index.ts index ddda5712c..56ef42216 100644 --- a/web/components/profile/preferences/index.ts +++ b/web/components/profile/preferences/index.ts @@ -1 +1 @@ -export * from "./email-notification-form"; \ No newline at end of file +export * from "./email-notification-form"; diff --git a/web/components/profile/profile-issues-filter.tsx b/web/components/profile/profile-issues-filter.tsx index 5b4f9c8ed..491c00f3a 100644 --- a/web/components/profile/profile-issues-filter.tsx +++ b/web/components/profile/profile-issues-filter.tsx @@ -4,9 +4,9 @@ import { useRouter } from "next/router"; // components import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, LayoutSelection } from "components/issues"; // hooks +import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { useIssues, useLabel } from "hooks/store"; // constants -import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; export const ProfileIssuesFilter = observer(() => { diff --git a/web/components/profile/profile-issues.tsx b/web/components/profile/profile-issues.tsx index 7e501764a..b6a99baf9 100644 --- a/web/components/profile/profile-issues.tsx +++ b/web/components/profile/profile-issues.tsx @@ -1,20 +1,20 @@ import React, { useEffect } from "react"; -import { useRouter } from "next/router"; -import useSWR from "swr"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { useTheme } from "next-themes"; +import useSWR from "swr"; // components -import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root"; -import { ProfileIssuesKanBanLayout } from "components/issues/issue-layouts/kanban/roots/profile-issues-root"; -import { IssuePeekOverview, ProfileIssuesAppliedFiltersRoot } from "components/issues"; -import { KanbanLayoutLoader, ListLayoutLoader } from "components/ui"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { IssuePeekOverview, ProfileIssuesAppliedFiltersRoot } from "components/issues"; +import { ProfileIssuesKanBanLayout } from "components/issues/issue-layouts/kanban/roots/profile-issues-root"; +import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root"; +import { KanbanLayoutLoader, ListLayoutLoader } from "components/ui"; // hooks +import { PROFILE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EIssuesStoreType } from "constants/issue"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { useIssues, useUser } from "hooks/store"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; -import { EIssuesStoreType } from "constants/issue"; -import { PROFILE_EMPTY_STATE_DETAILS } from "constants/empty-state"; interface IProfileIssuesPage { type: "assigned" | "subscribed" | "created"; @@ -41,8 +41,8 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => { } = useIssues(EIssuesStoreType.PROFILE); useEffect(() => { - setViewId(type); - }, [type]); + if (setViewId) setViewId(type); + }, [type, setViewId]); useSWR( workspaceSlug && userId ? `CURRENT_WORKSPACE_PROFILE_ISSUES_${workspaceSlug}_${userId}_${type}` : null, diff --git a/web/components/profile/sidebar.tsx b/web/components/profile/sidebar.tsx index 48bb7d323..e8b234cb8 100644 --- a/web/components/profile/sidebar.tsx +++ b/web/components/profile/sidebar.tsx @@ -1,25 +1,26 @@ import { useEffect, useRef } from "react"; -import { useRouter } from "next/router"; -import Link from "next/link"; -import useSWR from "swr"; -import { Disclosure, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import useSWR from "swr"; +// ui +import { Disclosure, Transition } from "@headlessui/react"; +// icons +import { ChevronDown, Pencil } from "lucide-react"; +// plane ui +import { Loader, Tooltip } from "@plane/ui"; +// fetch-keys +import { USER_PROFILE_PROJECT_SEGREGATION } from "constants/fetch-keys"; +// helpers +import { renderFormattedDate } from "helpers/date-time.helper"; +import { renderEmoji } from "helpers/emoji.helper"; // hooks -import useOutsideClickDetector from "hooks/use-outside-click-detector"; import { useApplication, useUser } from "hooks/store"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; // services import { UserService } from "services/user.service"; // components import { ProfileSidebarTime } from "./time"; -// ui -import { Loader, Tooltip } from "@plane/ui"; -// icons -import { ChevronDown, Pencil } from "lucide-react"; -// helpers -import { renderFormattedDate } from "helpers/date-time.helper"; -import { renderEmoji } from "helpers/emoji.helper"; -// fetch-keys -import { USER_PROFILE_PROJECT_SEGREGATION } from "constants/fetch-keys"; // services const userService = new UserService(); @@ -76,7 +77,7 @@ export const ProfileSidebar = observer(() => { return (
{userProjectsData ? ( @@ -106,7 +107,7 @@ export const ProfileSidebar = observer(() => { className="h-full w-full rounded object-cover" /> ) : ( -
+
{userProjectsData.user_data.first_name?.[0]}
)} diff --git a/web/components/project/card-list.tsx b/web/components/project/card-list.tsx index 624e30bde..a19b53fbb 100644 --- a/web/components/project/card-list.tsx +++ b/web/components/project/card-list.tsx @@ -1,14 +1,14 @@ import { observer } from "mobx-react-lite"; import { useTheme } from "next-themes"; // hooks -import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; // components -import { ProjectCard } from "components/project"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { ProjectCard } from "components/project"; import { ProjectsLoader } from "components/ui"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EUserWorkspaceRoles } from "constants/workspace"; +import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; export const ProjectCardList = observer(() => { // theme diff --git a/web/components/project/card.tsx b/web/components/project/card.tsx index 9f554cfea..c74e9ee75 100644 --- a/web/components/project/card.tsx +++ b/web/components/project/card.tsx @@ -1,21 +1,20 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { LinkIcon, Lock, Pencil, Star } from "lucide-react"; import Link from "next/link"; -// hooks -import { useProject } from "hooks/store"; -// components -import { DeleteProjectModal, JoinProjectModal } from "components/project"; +import { useRouter } from "next/router"; +import { LinkIcon, Lock, Pencil, Star } from "lucide-react"; // ui import { Avatar, AvatarGroup, Button, Tooltip, TOAST_TYPE, setToast, setPromiseToast } from "@plane/ui"; +// components +import { DeleteProjectModal, JoinProjectModal, EUserProjectRoles } from "components/project"; // helpers -import { copyTextToClipboard } from "helpers/string.helper"; import { renderEmoji } from "helpers/emoji.helper"; +import { copyTextToClipboard } from "helpers/string.helper"; +// hooks +import { useProject } from "hooks/store"; // types import type { IProject } from "@plane/types"; // constants -import { EUserProjectRoles } from "constants/project"; export type ProjectCardProps = { project: IProject; diff --git a/web/components/project/confirm-project-member-remove.tsx b/web/components/project/confirm-project-member-remove.tsx index 7ab4afa0a..2c94c092d 100644 --- a/web/components/project/confirm-project-member-remove.tsx +++ b/web/components/project/confirm-project-member-remove.tsx @@ -1,11 +1,11 @@ import React, { useState } from "react"; -import { Dialog, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; +import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; // hooks +import { Button } from "@plane/ui"; import { useUser } from "hooks/store"; // ui -import { Button } from "@plane/ui"; // types import { IUserLite } from "@plane/types"; @@ -94,8 +94,8 @@ export const ConfirmProjectMemberRemove: React.FC = observer((props) => { ? "Leaving..." : "Leave" : isDeleteLoading - ? "Removing..." - : "Remove"} + ? "Removing..." + : "Remove"}
diff --git a/web/components/project/create-project-modal.tsx b/web/components/project/create-project-modal.tsx index f7bbd92cf..01cbb5888 100644 --- a/web/components/project/create-project-modal.tsx +++ b/web/components/project/create-project-modal.tsx @@ -1,23 +1,22 @@ import { useState, useEffect, Fragment, FC, ChangeEvent } from "react"; +import { observer } from "mobx-react-lite"; import { useForm, Controller } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; -import { observer } from "mobx-react-lite"; import { X } from "lucide-react"; -// hooks -import { useEventTracker, useProject, useUser } from "hooks/store"; // ui import { Button, CustomSelect, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; // components import { ImagePickerPopover } from "components/core"; -import EmojiIconPicker from "components/emoji-icon-picker"; import { MemberDropdown } from "components/dropdowns"; +import EmojiIconPicker from "components/emoji-icon-picker"; +// constants +import { PROJECT_CREATED } from "constants/event-tracker"; +import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "constants/project"; +import { EUserWorkspaceRoles } from "constants/workspace"; // helpers import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper"; -// constants -import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "constants/project"; -// constants -import { EUserWorkspaceRoles } from "constants/workspace"; -import { PROJECT_CREATED } from "constants/event-tracker"; +// hooks +import { useEventTracker, useProject, useUser } from "hooks/store"; type Props = { isOpen: boolean; @@ -306,7 +305,7 @@ export const CreateProjectModal: FC = observer((props) => { onChange={handleIdentifierChange(onChange)} hasError={Boolean(errors.identifier)} placeholder="Identifier" - className="w-full text-xs focus:border-blue-400 uppercase" + className="w-full text-xs uppercase focus:border-blue-400" tabIndex={2} /> )} diff --git a/web/components/project/delete-project-modal.tsx b/web/components/project/delete-project-modal.tsx index 844bd3aad..bc26dcae4 100644 --- a/web/components/project/delete-project-modal.tsx +++ b/web/components/project/delete-project-modal.tsx @@ -4,13 +4,12 @@ import { Controller, useForm } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; // hooks -import { useEventTracker, useProject, useWorkspace } from "hooks/store"; -// ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; +import { useEventTracker, useProject } from "hooks/store"; +// ui // types import type { IProject } from "@plane/types"; // constants -import { PROJECT_DELETED } from "constants/event-tracker"; type DeleteProjectModal = { isOpen: boolean; @@ -27,7 +26,6 @@ export const DeleteProjectModal: React.FC = (props) => { const { isOpen, project, onClose } = props; // store hooks const { captureProjectEvent } = useEventTracker(); - const { currentWorkspace } = useWorkspace(); const { deleteProject } = useProject(); // router const router = useRouter(); diff --git a/web/components/project/form.tsx b/web/components/project/form.tsx index ef5a20024..25186e08e 100644 --- a/web/components/project/form.tsx +++ b/web/components/project/form.tsx @@ -1,23 +1,24 @@ import { FC, useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; -// hooks -import { useEventTracker, useProject } from "hooks/store"; -// components -import EmojiIconPicker from "components/emoji-icon-picker"; -import { ImagePickerPopover } from "components/core"; -import { Button, CustomSelect, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { Lock } from "lucide-react"; -// types -import { IProject, IWorkspace } from "@plane/types"; -// helpers -import { renderEmoji } from "helpers/emoji.helper"; -import { renderFormattedDate } from "helpers/date-time.helper"; +// ui +import { Button, CustomSelect, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { ImagePickerPopover } from "components/core"; +import EmojiIconPicker from "components/emoji-icon-picker"; // constants +import { PROJECT_UPDATED } from "constants/event-tracker"; import { NETWORK_CHOICES } from "constants/project"; +// helpers +import { renderFormattedDate } from "helpers/date-time.helper"; +import { renderEmoji } from "helpers/emoji.helper"; +// hooks +import { useEventTracker, useProject } from "hooks/store"; // services import { ProjectService } from "services/project"; -import { PROJECT_UPDATED } from "constants/event-tracker"; +// types +import { IProject, IWorkspace } from "@plane/types"; export interface IProjectDetailsForm { project: IProject; workspaceSlug: string; diff --git a/web/components/project/integration-card.tsx b/web/components/project/integration-card.tsx index cf256098f..84be19f71 100644 --- a/web/components/project/integration-card.tsx +++ b/web/components/project/integration-card.tsx @@ -1,24 +1,17 @@ import React from "react"; - import Image from "next/image"; - -import useSWR, { mutate } from "swr"; - -// services -import { ProjectService } from "services/project"; -// hooks import { useRouter } from "next/router"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { SelectRepository, SelectChannel } from "components/integration"; +// constants +import { PROJECT_GITHUB_REPOSITORY } from "constants/fetch-keys"; // icons import GithubLogo from "public/logos/github-square.png"; import SlackLogo from "public/services/slack.png"; -// ui -import { TOAST_TYPE, setToast } from "@plane/ui"; // types import { IWorkspaceIntegration } from "@plane/types"; -// fetch-keys -import { PROJECT_GITHUB_REPOSITORY } from "constants/fetch-keys"; type Props = { integration: IWorkspaceIntegration; diff --git a/web/components/project/join-project-modal.tsx b/web/components/project/join-project-modal.tsx index 58b549b6c..384333581 100644 --- a/web/components/project/join-project-modal.tsx +++ b/web/components/project/join-project-modal.tsx @@ -2,9 +2,9 @@ import { useState, Fragment } from "react"; import { useRouter } from "next/router"; import { Transition, Dialog } from "@headlessui/react"; // hooks +import { Button } from "@plane/ui"; import { useProject, useUser } from "hooks/store"; // ui -import { Button } from "@plane/ui"; // types import type { IProject } from "@plane/types"; diff --git a/web/components/project/leave-project-modal.tsx b/web/components/project/leave-project-modal.tsx index 45618d4f2..6982d6316 100644 --- a/web/components/project/leave-project-modal.tsx +++ b/web/components/project/leave-project-modal.tsx @@ -1,17 +1,19 @@ import { FC, Fragment } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; +// headless ui import { Dialog, Transition } from "@headlessui/react"; +// icons import { AlertTriangleIcon } from "lucide-react"; -import { observer } from "mobx-react-lite"; -// hooks -import { useEventTracker, useUser } from "hooks/store"; // ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; -// types -import { IProject } from "@plane/types"; // constants import { PROJECT_MEMBER_LEAVE } from "constants/event-tracker"; +// hooks +import { useEventTracker, useUser } from "hooks/store"; +// types +import { IProject } from "@plane/types"; type FormData = { projectName: string; diff --git a/web/components/project/member-list-item.tsx b/web/components/project/member-list-item.tsx index 6bab775b8..43c2ce2a8 100644 --- a/web/components/project/member-list-item.tsx +++ b/web/components/project/member-list-item.tsx @@ -1,19 +1,19 @@ import { useState } from "react"; -import { useRouter } from "next/router"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; -// hooks -import { useEventTracker, useMember, useProject, useUser } from "hooks/store"; -// components -import { ConfirmProjectMemberRemove } from "components/project"; -// ui -import { CustomSelect, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; +import Link from "next/link"; +import { useRouter } from "next/router"; // icons import { ChevronDown, Dot, XCircle } from "lucide-react"; +// ui +import { CustomSelect, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { ConfirmProjectMemberRemove } from "components/project"; // constants -import { ROLE } from "constants/workspace"; -import { EUserProjectRoles } from "constants/project"; import { PROJECT_MEMBER_LEAVE } from "constants/event-tracker"; +import { EUserProjectRoles } from "constants/project"; +import { ROLE } from "constants/workspace"; +// hooks +import { useEventTracker, useMember, useProject, useUser } from "hooks/store"; type Props = { userId: string; diff --git a/web/components/project/member-list.tsx b/web/components/project/member-list.tsx index d7b432445..7eaab01ef 100644 --- a/web/components/project/member-list.tsx +++ b/web/components/project/member-list.tsx @@ -2,12 +2,12 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; import { Search } from "lucide-react"; // hooks -import { useEventTracker, useMember } from "hooks/store"; // components +import { Button } from "@plane/ui"; import { ProjectMemberListItem, SendProjectInvitationModal } from "components/project"; // ui -import { Button } from "@plane/ui"; import { MembersSettingsLoader } from "components/ui"; +import { useEventTracker, useMember } from "hooks/store"; export const ProjectMemberList: React.FC = observer(() => { // states diff --git a/web/components/project/member-select.tsx b/web/components/project/member-select.tsx index 7dc9b3233..491dbce0a 100644 --- a/web/components/project/member-select.tsx +++ b/web/components/project/member-select.tsx @@ -2,9 +2,9 @@ import React from "react"; import { observer } from "mobx-react-lite"; import { Ban } from "lucide-react"; // hooks +import { Avatar, CustomSearchSelect } from "@plane/ui"; import { useMember } from "hooks/store"; // ui -import { Avatar, CustomSearchSelect } from "@plane/ui"; type Props = { value: any; diff --git a/web/components/project/project-settings-member-defaults.tsx b/web/components/project/project-settings-member-defaults.tsx index 91c06cdfc..d6cffa7a4 100644 --- a/web/components/project/project-settings-member-defaults.tsx +++ b/web/components/project/project-settings-member-defaults.tsx @@ -1,20 +1,19 @@ import { useEffect } from "react"; -import { useRouter } from "next/router"; -import useSWR from "swr"; import { observer } from "mobx-react-lite"; -// hooks -import { useProject, useUser } from "hooks/store"; +import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; - -import { MemberSelect } from "components/project"; +import useSWR from "swr"; // ui import { Loader, TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { MemberSelect } from "components/project"; +// constants +import { PROJECT_MEMBERS } from "constants/fetch-keys"; +import { EUserProjectRoles } from "constants/project"; +// hooks +import { useProject, useUser } from "hooks/store"; // types import { IProject, IUserLite, IWorkspace } from "@plane/types"; -// fetch-keys -import { PROJECT_MEMBERS } from "constants/fetch-keys"; -// constants -import { EUserProjectRoles } from "constants/project"; const defaultValues: Partial = { project_lead: null, diff --git a/web/components/project/publish-project/modal.tsx b/web/components/project/publish-project/modal.tsx index 64cf87fb5..048eb0306 100644 --- a/web/components/project/publish-project/modal.tsx +++ b/web/components/project/publish-project/modal.tsx @@ -1,17 +1,21 @@ import { Fragment, useEffect, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; +// ui import { Dialog, Transition } from "@headlessui/react"; +// icons import { Check, CircleDot, Globe2 } from "lucide-react"; -// hooks -import { useProjectPublish } from "hooks/store"; // ui import { Button, Loader, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; -import { CustomPopover } from "./popover"; +// hooks +import { useProjectPublish } from "hooks/store"; +// store +import { IProjectPublishSettings, TProjectPublishViews } from "store/project/project-publish.store"; // types import { IProject } from "@plane/types"; -import { IProjectPublishSettings, TProjectPublishViews } from "store/project/project-publish.store"; +// local components +import { CustomPopover } from "./popover"; type Props = { isOpen: boolean; @@ -359,16 +363,16 @@ export const PublishProjectModal: React.FC = observer((props) => { : "hover:bg-custom-background-80 hover:text-custom-text-100" }`} onClick={() => { - const _views = + const optionViews = value.length > 0 ? value.includes(option.key) ? value.filter((_o: string) => _o !== option.key) : [...value, option.key] : [option.key]; - if (_views.length === 0) return; + if (optionViews.length === 0) return; - onChange(_views); + onChange(optionViews); checkIfUpdateIsRequired(); }} > diff --git a/web/components/project/send-project-invitation-modal.tsx b/web/components/project/send-project-invitation-modal.tsx index da2f37e9f..76b92ec99 100644 --- a/web/components/project/send-project-invitation-modal.tsx +++ b/web/components/project/send-project-invitation-modal.tsx @@ -1,19 +1,15 @@ import React, { useEffect } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { useForm, Controller, useFieldArray } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; import { ChevronDown, Plus, X } from "lucide-react"; // hooks -import { useEventTracker, useMember, useUser, useWorkspace } from "hooks/store"; // ui import { Avatar, Button, CustomSelect, CustomSearchSelect, TOAST_TYPE, setToast } from "@plane/ui"; // helpers -import { getUserRole } from "helpers/user.helper"; +import { useEventTracker, useMember, useUser } from "hooks/store"; // constants -import { ROLE } from "constants/workspace"; -import { EUserProjectRoles } from "constants/project"; -import { PROJECT_MEMBER_ADDED } from "constants/event-tracker"; type Props = { isOpen: boolean; diff --git a/web/components/project/settings/delete-project-section.tsx b/web/components/project/settings/delete-project-section.tsx index fa1d70d5c..0f0e41fa7 100644 --- a/web/components/project/settings/delete-project-section.tsx +++ b/web/components/project/settings/delete-project-section.tsx @@ -2,9 +2,9 @@ import React from "react"; // ui import { Disclosure, Transition } from "@headlessui/react"; +import { ChevronDown, ChevronUp } from "lucide-react"; import { Button, Loader } from "@plane/ui"; // icons -import { ChevronDown, ChevronUp } from "lucide-react"; // types import { IProject } from "@plane/types"; diff --git a/web/components/project/settings/features-list.tsx b/web/components/project/settings/features-list.tsx index efbcc0857..188d103ba 100644 --- a/web/components/project/settings/features-list.tsx +++ b/web/components/project/settings/features-list.tsx @@ -1,17 +1,15 @@ import { FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { ContrastIcon, FileText, Inbox, Layers } from "lucide-react"; -// hooks -import { useEventTracker, useProject, useUser, useWorkspace } from "hooks/store"; // ui -import { DiceIcon, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; -// types -import { IProject } from "@plane/types"; +import { DiceIcon, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { EUserProjectRoles } from "constants/project"; - -type Props = {}; +// hooks +import { useEventTracker, useProject, useUser } from "hooks/store"; +// types +import { IProject } from "@plane/types"; const PROJECT_FEATURES_LIST = [ { @@ -46,7 +44,7 @@ const PROJECT_FEATURES_LIST = [ }, ]; -export const ProjectFeaturesList: FC = observer(() => { +export const ProjectFeaturesList: FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; diff --git a/web/components/project/sidebar-list-item.tsx b/web/components/project/sidebar-list-item.tsx index 00dc858d0..c86ba0dc2 100644 --- a/web/components/project/sidebar-list-item.tsx +++ b/web/components/project/sidebar-list-item.tsx @@ -1,9 +1,9 @@ import { useRef, useState } from "react"; +import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; -import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; import { Disclosure, Transition } from "@headlessui/react"; -import { observer } from "mobx-react-lite"; // icons import { MoreVertical, @@ -18,13 +18,6 @@ import { MoreHorizontal, Inbox, } from "lucide-react"; -// hooks -import { useApplication, useEventTracker, useInbox, useProject } from "hooks/store"; -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -// helpers -import { cn } from "helpers/common.helper"; -import { getNumberCount } from "helpers/string.helper"; -import { renderEmoji } from "helpers/emoji.helper"; // ui import { CustomMenu, @@ -36,9 +29,17 @@ import { LayersIcon, setPromiseToast, } from "@plane/ui"; -// components import { LeaveProjectModal, PublishProjectModal } from "components/project"; import { EUserProjectRoles } from "constants/project"; +import { cn } from "helpers/common.helper"; +import { renderEmoji } from "helpers/emoji.helper"; +import { getNumberCount } from "helpers/string.helper"; +// hooks +import { useApplication, useEventTracker, useInbox, useProject } from "hooks/store"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; +// helpers + +// components type Props = { projectId: string; diff --git a/web/components/project/sidebar-list.tsx b/web/components/project/sidebar-list.tsx index 05e09f565..2cee91e6b 100644 --- a/web/components/project/sidebar-list.tsx +++ b/web/components/project/sidebar-list.tsx @@ -1,21 +1,21 @@ -import { useState, FC, useRef, useEffect, useCallback } from "react"; -import { useRouter } from "next/router"; +import { useState, FC, useRef, useEffect } from "react"; import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd"; -import { Disclosure, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; +import { Disclosure, Transition } from "@headlessui/react"; import { ChevronDown, ChevronRight, Plus } from "lucide-react"; // hooks +import { TOAST_TYPE, setToast } from "@plane/ui"; +import { CreateProjectModal, ProjectSidebarListItem } from "components/project"; +import { EUserWorkspaceRoles } from "constants/workspace"; +import { cn } from "helpers/common.helper"; +import { orderJoinedProjects } from "helpers/project.helper"; +import { copyUrlToClipboard } from "helpers/string.helper"; import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; // ui -import { TOAST_TYPE, setToast } from "@plane/ui"; // components -import { CreateProjectModal, ProjectSidebarListItem } from "components/project"; // helpers -import { copyUrlToClipboard } from "helpers/string.helper"; -import { orderJoinedProjects } from "helpers/project.helper"; -import { cn } from "helpers/common.helper"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; import { IProject } from "@plane/types"; export const ProjectSidebarList: FC = observer(() => { @@ -63,8 +63,8 @@ export const ProjectSidebarList: FC = observer(() => { const joinedProjectsList: IProject[] = []; joinedProjects.map((projectId) => { - const _project = getProjectById(projectId); - if (_project) joinedProjectsList.push(_project); + const projectDetails = getProjectById(projectId); + if (projectDetails) joinedProjectsList.push(projectDetails); }); if (joinedProjectsList.length <= 0) return; diff --git a/web/components/states/create-state-modal.tsx b/web/components/states/create-state-modal.tsx index f39e3f335..b142cc60e 100644 --- a/web/components/states/create-state-modal.tsx +++ b/web/components/states/create-state-modal.tsx @@ -1,19 +1,19 @@ import React from "react"; -import { useRouter } from "next/router"; -import { Controller, useForm } from "react-hook-form"; -import { TwitterPicker } from "react-color"; -import { Dialog, Popover, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; -// hooks -import { useProjectState } from "hooks/store"; -// ui -import { Button, CustomSelect, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; +import { useRouter } from "next/router"; +import { TwitterPicker } from "react-color"; +import { Controller, useForm } from "react-hook-form"; +import { Dialog, Popover, Transition } from "@headlessui/react"; // icons import { ChevronDown } from "lucide-react"; -// types -import type { IState } from "@plane/types"; +// ui +import { Button, CustomSelect, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { GROUP_CHOICES } from "constants/project"; +// hooks +import { useProjectState } from "hooks/store"; +// types +import type { IState } from "@plane/types"; // types type Props = { diff --git a/web/components/states/create-update-state-inline.tsx b/web/components/states/create-update-state-inline.tsx index 0a50208cd..88c50a017 100644 --- a/web/components/states/create-update-state-inline.tsx +++ b/web/components/states/create-update-state-inline.tsx @@ -1,18 +1,18 @@ import React, { useEffect } from "react"; -import { useRouter } from "next/router"; -import { useForm, Controller } from "react-hook-form"; -import { TwitterPicker } from "react-color"; -import { Popover, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; -// hooks -import { useEventTracker, useProjectState } from "hooks/store"; +import { useRouter } from "next/router"; +import { TwitterPicker } from "react-color"; +import { useForm, Controller } from "react-hook-form"; +import { Popover, Transition } from "@headlessui/react"; // ui import { Button, CustomSelect, Input, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; +// constants +import { STATE_CREATED, STATE_UPDATED } from "constants/event-tracker"; +import { GROUP_CHOICES } from "constants/project"; +// hooks +import { useEventTracker, useProjectState } from "hooks/store"; // types import type { IState } from "@plane/types"; -// constants -import { GROUP_CHOICES } from "constants/project"; -import { STATE_CREATED, STATE_UPDATED } from "constants/event-tracker"; type Props = { data: IState | null; diff --git a/web/components/states/delete-state-modal.tsx b/web/components/states/delete-state-modal.tsx index df47c8b12..7496b53b4 100644 --- a/web/components/states/delete-state-modal.tsx +++ b/web/components/states/delete-state-modal.tsx @@ -1,16 +1,16 @@ import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; -import { observer } from "mobx-react-lite"; import { AlertTriangle } from "lucide-react"; -// hooks -import { useEventTracker, useProjectState } from "hooks/store"; // ui import { Button, TOAST_TYPE, setToast } from "@plane/ui"; -// types -import type { IState } from "@plane/types"; // constants import { STATE_DELETED } from "constants/event-tracker"; +// hooks +import { useEventTracker, useProjectState } from "hooks/store"; +// types +import type { IState } from "@plane/types"; type Props = { isOpen: boolean; diff --git a/web/components/states/project-setting-state-list-item.tsx b/web/components/states/project-setting-state-list-item.tsx index 401c482f3..760c8501c 100644 --- a/web/components/states/project-setting-state-list-item.tsx +++ b/web/components/states/project-setting-state-list-item.tsx @@ -1,14 +1,14 @@ import { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks -import { useEventTracker, useProjectState } from "hooks/store"; // ui +import { Pencil, X, ArrowDown, ArrowUp } from "lucide-react"; import { Tooltip, StateGroupIcon } from "@plane/ui"; // icons -import { Pencil, X, ArrowDown, ArrowUp } from "lucide-react"; // helpers import { addSpaceIfCamelCase } from "helpers/string.helper"; +import { useEventTracker, useProjectState } from "hooks/store"; // types import { IState } from "@plane/types"; diff --git a/web/components/states/project-setting-state-list.tsx b/web/components/states/project-setting-state-list.tsx index 99ac40d84..5f7772567 100644 --- a/web/components/states/project-setting-state-list.tsx +++ b/web/components/states/project-setting-state-list.tsx @@ -1,20 +1,20 @@ import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react-lite"; // hooks +import { Plus } from "lucide-react"; +import { Loader } from "@plane/ui"; +import { CreateUpdateStateInline, DeleteStateModal, StateGroup, StatesListItem } from "components/states"; +import { STATES_LIST } from "constants/fetch-keys"; +import { sortByField } from "helpers/array.helper"; +import { orderStateGroups } from "helpers/state.helper"; import { useEventTracker, useProjectState } from "hooks/store"; // components -import { CreateUpdateStateInline, DeleteStateModal, StateGroup, StatesListItem } from "components/states"; // ui -import { Loader } from "@plane/ui"; // icons -import { Plus } from "lucide-react"; // helpers -import { orderStateGroups } from "helpers/state.helper"; -import { sortByField } from "helpers/array.helper"; // fetch-keys -import { STATES_LIST } from "constants/fetch-keys"; export const ProjectSettingStateList: React.FC = observer(() => { // router diff --git a/web/components/toast-alert/index.tsx b/web/components/toast-alert/index.tsx new file mode 100644 index 000000000..80594e218 --- /dev/null +++ b/web/components/toast-alert/index.tsx @@ -0,0 +1,61 @@ +import React from "react"; +// hooks +import { AlertTriangle, CheckCircle, Info, X, XCircle } from "lucide-react"; +import useToast from "hooks/use-toast"; +// icons + +const ToastAlerts = () => { + const { alerts, removeAlert } = useToast(); + + if (!alerts) return null; + + return ( +
+ {alerts.map((alert) => ( +
+
+ +
+
+
+
+ {alert.type === "success" ? ( +
+
+

{alert.title}

+ {alert.message &&

{alert.message}

} +
+
+
+
+ ))} +
+ ); +}; + +export default ToastAlerts; diff --git a/web/components/ui/empty-space.tsx b/web/components/ui/empty-space.tsx index 4b70bbb15..73fc6ba01 100644 --- a/web/components/ui/empty-space.tsx +++ b/web/components/ui/empty-space.tsx @@ -1,7 +1,7 @@ // next +import React from "react"; import Link from "next/link"; // react -import React from "react"; // icons import { ChevronRight } from "lucide-react"; diff --git a/web/components/ui/graphs/bar-graph.tsx b/web/components/ui/graphs/bar-graph.tsx index 3756b0455..3f40aad87 100644 --- a/web/components/ui/graphs/bar-graph.tsx +++ b/web/components/ui/graphs/bar-graph.tsx @@ -1,11 +1,11 @@ // nivo import { ResponsiveBar, BarSvgProps } from "@nivo/bar"; // helpers +import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; import { generateYAxisTickValues } from "helpers/graph.helper"; // types import { TGraph } from "./types"; // constants -import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; type Props = { indexBy: string; diff --git a/web/components/ui/graphs/calendar-graph.tsx b/web/components/ui/graphs/calendar-graph.tsx index 0725c425a..a64a4a920 100644 --- a/web/components/ui/graphs/calendar-graph.tsx +++ b/web/components/ui/graphs/calendar-graph.tsx @@ -1,9 +1,9 @@ // nivo import { ResponsiveCalendar, CalendarSvgProps } from "@nivo/calendar"; // types +import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; import { TGraph } from "./types"; // constants -import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; export const CalendarGraph: React.FC> = ({ height = "400px", diff --git a/web/components/ui/graphs/line-graph.tsx b/web/components/ui/graphs/line-graph.tsx index 91a19acc3..93eac0097 100644 --- a/web/components/ui/graphs/line-graph.tsx +++ b/web/components/ui/graphs/line-graph.tsx @@ -1,11 +1,11 @@ // nivo import { ResponsiveLine, LineSvgProps } from "@nivo/line"; // helpers +import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; import { generateYAxisTickValues } from "helpers/graph.helper"; // types import { TGraph } from "./types"; // constants -import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; type Props = { customYAxisTickValues?: number[]; diff --git a/web/components/ui/graphs/marimekko-graph.tsx b/web/components/ui/graphs/marimekko-graph.tsx new file mode 100644 index 000000000..c0e6eb300 --- /dev/null +++ b/web/components/ui/graphs/marimekko-graph.tsx @@ -0,0 +1,48 @@ +// nivo +import { ResponsiveMarimekko, SvgProps } from "@nivo/marimekko"; +// helpers +import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; +import { generateYAxisTickValues } from "helpers/graph.helper"; +// types +import { TGraph } from "./types"; +// constants + +type Props = { + id: string; + value: string; + customYAxisTickValues?: number[]; +}; + +export const MarimekkoGraph: React.FC, "height" | "width">> = ({ + id, + value, + customYAxisTickValues, + height = "400px", + width = "100%", + margin, + theme, + ...rest +}) => ( +
+ 7 ? -45 : 0, + }} + labelTextColor={{ from: "color", modifiers: [["darker", 1.6]] }} + theme={{ ...CHARTS_THEME, ...(theme ?? {}) }} + animate + {...rest} + /> +
+); diff --git a/web/components/ui/graphs/pie-graph.tsx b/web/components/ui/graphs/pie-graph.tsx index 52b56e492..739ede4b0 100644 --- a/web/components/ui/graphs/pie-graph.tsx +++ b/web/components/ui/graphs/pie-graph.tsx @@ -1,9 +1,9 @@ // nivo import { PieSvgProps, ResponsivePie } from "@nivo/pie"; // types +import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; import { TGraph } from "./types"; // constants -import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; export const PieGraph: React.FC, "height" | "width">> = ({ height = "400px", diff --git a/web/components/ui/graphs/scatter-plot-graph.tsx b/web/components/ui/graphs/scatter-plot-graph.tsx index c6ff5a772..4eb82a97e 100644 --- a/web/components/ui/graphs/scatter-plot-graph.tsx +++ b/web/components/ui/graphs/scatter-plot-graph.tsx @@ -1,9 +1,9 @@ // nivo import { ResponsiveScatterPlot, ScatterPlotSvgProps } from "@nivo/scatterplot"; // types +import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; import { TGraph } from "./types"; // constants -import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; export const ScatterPlotGraph: React.FC, "height" | "width">> = ({ height = "400px", diff --git a/web/components/ui/loader/cycle-module-board-loader.tsx b/web/components/ui/loader/cycle-module-board-loader.tsx index 09c885fb9..f88719c38 100644 --- a/web/components/ui/loader/cycle-module-board-loader.tsx +++ b/web/components/ui/loader/cycle-module-board-loader.tsx @@ -2,8 +2,11 @@ export const CycleModuleBoardLayout = () => (
- {[...Array(5)].map(() => ( -
+ {[...Array(5)].map((i) => ( +
diff --git a/web/components/ui/loader/cycle-module-list-loader.tsx b/web/components/ui/loader/cycle-module-list-loader.tsx index 8787a1425..522b96f0d 100644 --- a/web/components/ui/loader/cycle-module-list-loader.tsx +++ b/web/components/ui/loader/cycle-module-list-loader.tsx @@ -2,8 +2,11 @@ export const CycleModuleListLayout = () => (
- {[...Array(5)].map(() => ( -
+ {[...Array(5)].map((i) => ( +
diff --git a/web/components/ui/loader/layouts/project-inbox/inbox-layout-loader.tsx b/web/components/ui/loader/layouts/project-inbox/inbox-layout-loader.tsx index 944ec02b8..3456e43ab 100644 --- a/web/components/ui/loader/layouts/project-inbox/inbox-layout-loader.tsx +++ b/web/components/ui/loader/layouts/project-inbox/inbox-layout-loader.tsx @@ -1,7 +1,7 @@ import React from "react"; // ui -import { InboxSidebarLoader } from "./inbox-sidebar-loader"; import { Loader } from "@plane/ui"; +import { InboxSidebarLoader } from "./inbox-sidebar-loader"; export const InboxLayoutLoader = () => (
diff --git a/web/components/ui/loader/layouts/project-inbox/inbox-sidebar-loader.tsx b/web/components/ui/loader/layouts/project-inbox/inbox-sidebar-loader.tsx index ce464e83d..204c2fff6 100644 --- a/web/components/ui/loader/layouts/project-inbox/inbox-sidebar-loader.tsx +++ b/web/components/ui/loader/layouts/project-inbox/inbox-sidebar-loader.tsx @@ -7,8 +7,8 @@ export const InboxSidebarLoader = () => (
- {[...Array(6)].map(() => ( -
+ {[...Array(6)].map((i) => ( +
diff --git a/web/components/ui/loader/notification-loader.tsx b/web/components/ui/loader/notification-loader.tsx index 143f1a9b6..7485c2c4c 100644 --- a/web/components/ui/loader/notification-loader.tsx +++ b/web/components/ui/loader/notification-loader.tsx @@ -1,7 +1,7 @@ export const NotificationsLoader = () => (
- {[...Array(3)].map(() => ( -
+ {[...Array(3)].map((i) => ( +
diff --git a/web/components/ui/loader/pages-loader.tsx b/web/components/ui/loader/pages-loader.tsx index e31e82942..612c17d88 100644 --- a/web/components/ui/loader/pages-loader.tsx +++ b/web/components/ui/loader/pages-loader.tsx @@ -4,13 +4,13 @@ export const PagesLoader = () => (

Pages

- {[...Array(5)].map(() => ( - + {[...Array(5)].map((i) => ( + ))}
- {[...Array(5)].map(() => ( -
+ {[...Array(5)].map((i) => ( +
diff --git a/web/components/ui/loader/projects-loader.tsx b/web/components/ui/loader/projects-loader.tsx index 9548a1f48..d1a781d6b 100644 --- a/web/components/ui/loader/projects-loader.tsx +++ b/web/components/ui/loader/projects-loader.tsx @@ -1,8 +1,11 @@ export const ProjectsLoader = () => (
- {[...Array(3)].map(() => ( -
+ {[...Array(3)].map((i) => ( +
diff --git a/web/components/ui/loader/settings/activity.tsx b/web/components/ui/loader/settings/activity.tsx index 7bc5c392f..70297f644 100644 --- a/web/components/ui/loader/settings/activity.tsx +++ b/web/components/ui/loader/settings/activity.tsx @@ -2,8 +2,8 @@ import { getRandomLength } from "../utils"; export const ActivitySettingsLoader = () => (
- {[...Array(10)].map(() => ( -
+ {[...Array(10)].map((i) => ( +
diff --git a/web/components/ui/loader/settings/api-token.tsx b/web/components/ui/loader/settings/api-token.tsx index fc5b4c41d..e31090bff 100644 --- a/web/components/ui/loader/settings/api-token.tsx +++ b/web/components/ui/loader/settings/api-token.tsx @@ -5,8 +5,8 @@ export const APITokenSettingsLoader = () => (
- {[...Array(2)].map(() => ( -
+ {[...Array(2)].map((i) => ( +
diff --git a/web/components/ui/loader/settings/email.tsx b/web/components/ui/loader/settings/email.tsx index fa68b972f..87634bf09 100644 --- a/web/components/ui/loader/settings/email.tsx +++ b/web/components/ui/loader/settings/email.tsx @@ -8,8 +8,8 @@ export const EmailSettingsLoader = () => (
- {[...Array(4)].map(() => ( -
+ {[...Array(4)].map((i) => ( +
diff --git a/web/components/ui/loader/settings/import-and-export.tsx b/web/components/ui/loader/settings/import-and-export.tsx index 70496d1c1..a3561207d 100644 --- a/web/components/ui/loader/settings/import-and-export.tsx +++ b/web/components/ui/loader/settings/import-and-export.tsx @@ -1,7 +1,7 @@ export const ImportExportSettingsLoader = () => (
- {[...Array(2)].map(() => ( -
+ {[...Array(2)].map((i) => ( +
diff --git a/web/components/ui/loader/settings/integration.tsx b/web/components/ui/loader/settings/integration.tsx index 871b570b1..2260517ee 100644 --- a/web/components/ui/loader/settings/integration.tsx +++ b/web/components/ui/loader/settings/integration.tsx @@ -1,7 +1,10 @@ export const IntegrationsSettingsLoader = () => (
- {[...Array(2)].map(() => ( -
+ {[...Array(2)].map((i) => ( +
diff --git a/web/components/ui/loader/settings/members.tsx b/web/components/ui/loader/settings/members.tsx index 3ed2c41ef..e286320a9 100644 --- a/web/components/ui/loader/settings/members.tsx +++ b/web/components/ui/loader/settings/members.tsx @@ -1,7 +1,7 @@ export const MembersSettingsLoader = () => (
- {[...Array(4)].map(() => ( -
+ {[...Array(4)].map((i) => ( +
diff --git a/web/components/ui/loader/view-list-loader.tsx b/web/components/ui/loader/view-list-loader.tsx index 97899a657..8b59b57a2 100644 --- a/web/components/ui/loader/view-list-loader.tsx +++ b/web/components/ui/loader/view-list-loader.tsx @@ -1,7 +1,7 @@ export const ViewListLoader = () => (
- {[...Array(8)].map(() => ( -
+ {[...Array(8)].map((i) => ( +
diff --git a/web/components/ui/multi-level-dropdown.tsx b/web/components/ui/multi-level-dropdown.tsx index 7bf4aa8a1..8bb0ebcf3 100644 --- a/web/components/ui/multi-level-dropdown.tsx +++ b/web/components/ui/multi-level-dropdown.tsx @@ -3,9 +3,9 @@ import { Fragment, useState } from "react"; // headless ui import { Menu, Transition } from "@headlessui/react"; // ui +import { Check, ChevronDown, ChevronLeft, ChevronRight } from "lucide-react"; import { Loader } from "@plane/ui"; // icons -import { Check, ChevronDown, ChevronLeft, ChevronRight } from "lucide-react"; type MultiLevelDropdownProps = { label: string; @@ -71,10 +71,10 @@ export const MultiLevelDropdown: React.FC = ({
{ + onClick={(e: unknown) => { if (option.hasChildren) { - e.stopPropagation(); - e.preventDefault(); + e?.stopPropagation(); + e?.preventDefault(); if (option.onClick) option.onClick(); diff --git a/web/components/views/delete-view-modal.tsx b/web/components/views/delete-view-modal.tsx index f4fe8e120..180c293e0 100644 --- a/web/components/views/delete-view-modal.tsx +++ b/web/components/views/delete-view-modal.tsx @@ -1,12 +1,12 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; -// hooks -import { useProjectView } from "hooks/store"; // ui import { Button, TOAST_TYPE, setToast } from "@plane/ui"; +// hooks +import { useProjectView } from "hooks/store"; // types import { IProjectView } from "@plane/types"; diff --git a/web/components/views/form.tsx b/web/components/views/form.tsx index 0da7e3946..31fee1006 100644 --- a/web/components/views/form.tsx +++ b/web/components/views/form.tsx @@ -2,15 +2,15 @@ import { useEffect } from "react"; import { observer } from "mobx-react-lite"; import { Controller, useForm } from "react-hook-form"; // hooks +import { Button, Input, TextArea } from "@plane/ui"; +import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "components/issues"; +import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { useLabel, useMember, useProjectState } from "hooks/store"; // components -import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "components/issues"; // ui -import { Button, Input, TextArea } from "@plane/ui"; // types import { IProjectView, IIssueFilterOptions } from "@plane/types"; // constants -import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; type Props = { data?: IProjectView | null; @@ -212,8 +212,8 @@ export const ProjectViewForm: React.FC = observer((props) => { ? "Updating View..." : "Update View" : isSubmitting - ? "Creating View..." - : "Create View"} + ? "Creating View..." + : "Create View"}
diff --git a/web/components/views/modal.tsx b/web/components/views/modal.tsx index a1abef1a4..7e0c92f26 100644 --- a/web/components/views/modal.tsx +++ b/web/components/views/modal.tsx @@ -1,12 +1,12 @@ import { FC, Fragment } from "react"; import { observer } from "mobx-react-lite"; import { Dialog, Transition } from "@headlessui/react"; -// hooks -import { useProjectView } from "hooks/store"; // ui import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { ProjectViewForm } from "components/views"; +// hooks +import { useProjectView } from "hooks/store"; // types import { IProjectView } from "@plane/types"; diff --git a/web/components/views/view-list-item.tsx b/web/components/views/view-list-item.tsx index 7ff1ee92e..29d5bac57 100644 --- a/web/components/views/view-list-item.tsx +++ b/web/components/views/view-list-item.tsx @@ -1,21 +1,21 @@ import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import { LinkIcon, PencilIcon, StarIcon, TrashIcon } from "lucide-react"; -// hooks -import { useProjectView, useUser } from "hooks/store"; -// components -import { CreateUpdateProjectViewModal, DeleteProjectViewModal } from "components/views"; // ui import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { CreateUpdateProjectViewModal, DeleteProjectViewModal } from "components/views"; +// constants +import { EUserProjectRoles } from "constants/project"; // helpers import { calculateTotalFilters } from "helpers/filter.helper"; import { copyUrlToClipboard } from "helpers/string.helper"; +// hooks +import { useProjectView, useUser } from "hooks/store"; // types import { IProjectView } from "@plane/types"; -// constants -import { EUserProjectRoles } from "constants/project"; type Props = { view: IProjectView; diff --git a/web/components/views/views-list.tsx b/web/components/views/views-list.tsx index 4977ed3e7..9d8bf85e6 100644 --- a/web/components/views/views-list.tsx +++ b/web/components/views/views-list.tsx @@ -1,18 +1,18 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; -import { Search } from "lucide-react"; import { useTheme } from "next-themes"; +import { Search } from "lucide-react"; // hooks -import { useApplication, useProjectView, useUser } from "hooks/store"; // components -import { ProjectViewListItem } from "components/views"; +import { Input } from "@plane/ui"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui -import { Input } from "@plane/ui"; import { ViewListLoader } from "components/ui"; +import { ProjectViewListItem } from "components/views"; // constants -import { EUserProjectRoles } from "constants/project"; import { VIEW_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EUserProjectRoles } from "constants/project"; +import { useApplication, useProjectView, useUser } from "hooks/store"; export const ProjectViewsList = observer(() => { // states diff --git a/web/components/web-hooks/create-webhook-modal.tsx b/web/components/web-hooks/create-webhook-modal.tsx index ecbd4ccd3..b18beede8 100644 --- a/web/components/web-hooks/create-webhook-modal.tsx +++ b/web/components/web-hooks/create-webhook-modal.tsx @@ -1,18 +1,18 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; +// ui import { Dialog, Transition } from "@headlessui/react"; +import { TOAST_TYPE, setToast } from "@plane/ui"; // components -import { WebhookForm } from "./form"; -import { GeneratedHookDetails } from "./generated-hook-details"; -// hooks // helpers import { csvDownload } from "helpers/download.helper"; +// types +import { IWebhook, IWorkspace, TWebhookEventTypes } from "@plane/types"; +import { WebhookForm } from "./form"; +import { GeneratedHookDetails } from "./generated-hook-details"; // utils import { getCurrentHookAsCSV } from "./utils"; // ui -import { TOAST_TYPE, setToast } from "@plane/ui"; -// types -import { IWebhook, IWorkspace, TWebhookEventTypes } from "@plane/types"; interface ICreateWebhookModal { currentWorkspace: IWorkspace | null; diff --git a/web/components/web-hooks/delete-webhook-modal.tsx b/web/components/web-hooks/delete-webhook-modal.tsx index 52c7a6595..22f8aca32 100644 --- a/web/components/web-hooks/delete-webhook-modal.tsx +++ b/web/components/web-hooks/delete-webhook-modal.tsx @@ -2,10 +2,10 @@ import React, { FC, useState } from "react"; import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; -// hooks -import { useWebhook } from "hooks/store"; // ui import { Button, TOAST_TYPE, setToast } from "@plane/ui"; +// hooks +import { useWebhook } from "hooks/store"; interface IDeleteWebhook { isOpen: boolean; diff --git a/web/components/web-hooks/form/form.tsx b/web/components/web-hooks/form/form.tsx index c2dd940dc..1b1e1bf27 100644 --- a/web/components/web-hooks/form/form.tsx +++ b/web/components/web-hooks/form/form.tsx @@ -1,9 +1,8 @@ import React, { FC, useEffect, useState } from "react"; -import { Controller, useForm } from "react-hook-form"; import { observer } from "mobx-react-lite"; +import { Controller, useForm } from "react-hook-form"; // hooks -import { useWebhook } from "hooks/store"; -// components +import { Button } from "@plane/ui"; import { WebhookIndividualEventOptions, WebhookInput, @@ -11,8 +10,9 @@ import { WebhookSecretKey, WebhookToggle, } from "components/web-hooks"; +import { useWebhook } from "hooks/store"; +// components // ui -import { Button } from "@plane/ui"; // types import { IWebhook, TWebhookEventTypes } from "@plane/types"; @@ -36,7 +36,7 @@ export const WebhookForm: FC = observer((props) => { // states const [webhookEventType, setWebhookEventType] = useState("all"); // store hooks - const {webhookSecretKey } = useWebhook(); + const { webhookSecretKey } = useWebhook(); // use form const { handleSubmit, diff --git a/web/components/web-hooks/form/secret-key.tsx b/web/components/web-hooks/form/secret-key.tsx index 7e9d9deda..11129fb07 100644 --- a/web/components/web-hooks/form/secret-key.tsx +++ b/web/components/web-hooks/form/secret-key.tsx @@ -1,18 +1,19 @@ import { useState, FC } from "react"; -import { useRouter } from "next/router"; -import { Copy, Eye, EyeOff, RefreshCw } from "lucide-react"; import { observer } from "mobx-react-lite"; -// hooks -import { useWebhook, useWorkspace } from "hooks/store"; -// helpers -import { copyTextToClipboard } from "helpers/string.helper"; -import { csvDownload } from "helpers/download.helper"; -// utils -import { getCurrentHookAsCSV } from "../utils"; +import { useRouter } from "next/router"; +// icons +import { Copy, Eye, EyeOff, RefreshCw } from "lucide-react"; // ui import { Button, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; +// helpers +import { csvDownload } from "helpers/download.helper"; +import { copyTextToClipboard } from "helpers/string.helper"; +// hooks +import { useWebhook, useWorkspace } from "hooks/store"; // types import { IWebhook } from "@plane/types"; +// utils +import { getCurrentHookAsCSV } from "../utils"; type Props = { data: Partial; diff --git a/web/components/web-hooks/generated-hook-details.tsx b/web/components/web-hooks/generated-hook-details.tsx index ce78fa5d5..2cd5ef986 100644 --- a/web/components/web-hooks/generated-hook-details.tsx +++ b/web/components/web-hooks/generated-hook-details.tsx @@ -1,9 +1,9 @@ // components -import { WebhookSecretKey } from "./form"; // ui import { Button } from "@plane/ui"; // types import { IWebhook } from "@plane/types"; +import { WebhookSecretKey } from "./form"; type Props = { handleClose: () => void; diff --git a/web/components/web-hooks/webhooks-list-item.tsx b/web/components/web-hooks/webhooks-list-item.tsx index fa676fccd..2f9ca52a5 100644 --- a/web/components/web-hooks/webhooks-list-item.tsx +++ b/web/components/web-hooks/webhooks-list-item.tsx @@ -2,9 +2,9 @@ import { FC } from "react"; import Link from "next/link"; import { useRouter } from "next/router"; // hooks +import { ToggleSwitch } from "@plane/ui"; import { useWebhook } from "hooks/store"; // ui -import { ToggleSwitch } from "@plane/ui"; // types import { IWebhook } from "@plane/types"; diff --git a/web/components/workspace/confirm-workspace-member-remove.tsx b/web/components/workspace/confirm-workspace-member-remove.tsx index 6c5ec4593..a11938472 100644 --- a/web/components/workspace/confirm-workspace-member-remove.tsx +++ b/web/components/workspace/confirm-workspace-member-remove.tsx @@ -3,9 +3,9 @@ import { observer } from "mobx-react-lite"; import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; // hooks +import { Button } from "@plane/ui"; import { useUser } from "hooks/store"; // ui -import { Button } from "@plane/ui"; type Props = { isOpen: boolean; @@ -102,8 +102,8 @@ export const ConfirmWorkspaceMemberRemove: React.FC = observer((props) => ? "Leaving" : "Leave" : isRemoving - ? "Removing" - : "Remove"} + ? "Removing" + : "Remove"}
diff --git a/web/components/workspace/create-workspace-form.tsx b/web/components/workspace/create-workspace-form.tsx index e8e40cf85..822ee1347 100644 --- a/web/components/workspace/create-workspace-form.tsx +++ b/web/components/workspace/create-workspace-form.tsx @@ -1,18 +1,17 @@ import { Dispatch, SetStateAction, useEffect, useState, FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; -// services -import { WorkspaceService } from "services/workspace.service"; +// ui +import { Button, CustomSelect, Input, TOAST_TYPE, setToast } from "@plane/ui"; +// constants +import { WORKSPACE_CREATED } from "constants/event-tracker"; +import { ORGANIZATION_SIZE, RESTRICTED_URLS } from "constants/workspace"; // hooks import { useEventTracker, useWorkspace } from "hooks/store"; // ui -import { Button, CustomSelect, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IWorkspace } from "@plane/types"; -// constants -import { ORGANIZATION_SIZE, RESTRICTED_URLS } from "constants/workspace"; -import { WORKSPACE_CREATED } from "constants/event-tracker"; type Props = { onSubmit?: (res: IWorkspace) => Promise; @@ -21,7 +20,7 @@ type Props = { slug: string; organization_size: string; }; - setDefaultValues: Dispatch>; + setDefaultValues: Dispatch>; secondaryButton?: React.ReactNode; primaryButtonText?: { loading: string; diff --git a/web/components/workspace/delete-workspace-modal.tsx b/web/components/workspace/delete-workspace-modal.tsx index dbb2ef4f0..0691fbbf0 100644 --- a/web/components/workspace/delete-workspace-modal.tsx +++ b/web/components/workspace/delete-workspace-modal.tsx @@ -1,17 +1,17 @@ import React from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; -// hooks -import { useEventTracker, useWorkspace } from "hooks/store"; // ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; -// types -import type { IWorkspace } from "@plane/types"; // constants import { WORKSPACE_DELETED } from "constants/event-tracker"; +// hooks +import { useEventTracker, useWorkspace } from "hooks/store"; +// types +import type { IWorkspace } from "@plane/types"; type Props = { isOpen: boolean; @@ -55,7 +55,7 @@ export const DeleteWorkspaceModal: React.FC = observer((props) => { if (!data || !canDelete) return; await deleteWorkspace(data.slug) - .then((res) => { + .then(() => { handleClose(); router.push("/"); captureWorkspaceEvent({ diff --git a/web/components/workspace/help-section.tsx b/web/components/workspace/help-section.tsx index 0bb77f9c7..210bbbd3a 100644 --- a/web/components/workspace/help-section.tsx +++ b/web/components/workspace/help-section.tsx @@ -1,17 +1,19 @@ import React, { useRef, useState } from "react"; -import Link from "next/link"; -import { Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; +// headless ui +import { Transition } from "@headlessui/react"; +// icons +import { FileText, HelpCircle, MessagesSquare, MoveLeft, Zap } from "lucide-react"; +// ui +import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui"; // hooks import { useApplication } from "hooks/store"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; -// icons -import { FileText, HelpCircle, MessagesSquare, MoveLeft, Zap } from "lucide-react"; -import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui"; // assets import packageJson from "package.json"; -const helpOptions = [ +const HELP_OPTIONS = [ { name: "Documentation", href: "https://docs.plane.so/", @@ -27,12 +29,6 @@ const helpOptions = [ href: "https://github.com/makeplane/plane/issues/new/choose", Icon: GithubIcon, }, - { - name: "Chat with us", - href: null, - onClick: () => (window as any).$crisp.push(["do", "chat:show"]), - Icon: MessagesSquare, - }, ]; export interface WorkspaceHelpSectionProps { @@ -45,12 +41,17 @@ export const WorkspaceHelpSection: React.FC = observe theme: { sidebarCollapsed, toggleSidebar }, commandPalette: { toggleShortcutModal }, } = useApplication(); - // states const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false); // refs const helpOptionsRef = useRef(null); + const handleCrispWindowShow = () => { + if (window) { + window.$crisp.push(["do", "chat:show"]); + } + }; + useOutsideClickDetector(helpOptionsRef, () => setIsNeedHelpOpen(false)); const isCollapsed = sidebarCollapsed || false; @@ -129,33 +130,26 @@ export const WorkspaceHelpSection: React.FC = observe ref={helpOptionsRef} >
- {helpOptions.map(({ name, Icon, href, onClick }) => { - if (href) - return ( - - -
- -
- {name} -
- - ); - else - return ( - - ); - })} + {HELP_OPTIONS.map(({ name, Icon, href }) => ( + + +
+ +
+ {name} +
+ + ))} +
Version: v{packageJson.version}
diff --git a/web/components/workspace/send-workspace-invitation-modal.tsx b/web/components/workspace/send-workspace-invitation-modal.tsx index 25f4c3c72..55f64bfab 100644 --- a/web/components/workspace/send-workspace-invitation-modal.tsx +++ b/web/components/workspace/send-workspace-invitation-modal.tsx @@ -3,14 +3,14 @@ import { observer } from "mobx-react-lite"; import { Controller, useFieldArray, useForm } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; import { Plus, X } from "lucide-react"; -// hooks -import { useUser } from "hooks/store"; // ui import { Button, CustomSelect, Input } from "@plane/ui"; -// types -import { IWorkspaceBulkInviteFormData } from "@plane/types"; // constants import { EUserWorkspaceRoles, ROLE } from "constants/workspace"; +// hooks +import { useUser } from "hooks/store"; +// types +import { IWorkspaceBulkInviteFormData } from "@plane/types"; type Props = { isOpen: boolean; diff --git a/web/components/workspace/settings/invitations-list-item.tsx b/web/components/workspace/settings/invitations-list-item.tsx index 9a9df5cb1..8c6de24b2 100644 --- a/web/components/workspace/settings/invitations-list-item.tsx +++ b/web/components/workspace/settings/invitations-list-item.tsx @@ -1,15 +1,15 @@ import { useState, FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { ChevronDown, XCircle } from "lucide-react"; -// hooks -import { useMember, useUser } from "hooks/store"; -// components -import { ConfirmWorkspaceMemberRemove } from "components/workspace"; // ui import { CustomSelect, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { ConfirmWorkspaceMemberRemove } from "components/workspace"; // constants import { EUserWorkspaceRoles, ROLE } from "constants/workspace"; +// hooks +import { useMember, useUser } from "hooks/store"; type Props = { invitationId: string; diff --git a/web/components/workspace/settings/members-list-item.tsx b/web/components/workspace/settings/members-list-item.tsx index c6c8d1d36..f40d78bb0 100644 --- a/web/components/workspace/settings/members-list-item.tsx +++ b/web/components/workspace/settings/members-list-item.tsx @@ -1,17 +1,18 @@ import { useState, FC } from "react"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; +// lucide icons import { ChevronDown, Dot, XCircle } from "lucide-react"; -// hooks -import { useEventTracker, useMember, useUser } from "hooks/store"; -// components -import { ConfirmWorkspaceMemberRemove } from "components/workspace"; // ui import { CustomSelect, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { ConfirmWorkspaceMemberRemove } from "components/workspace"; // constants -import { EUserWorkspaceRoles, ROLE } from "constants/workspace"; import { WORKSPACE_MEMBER_lEAVE } from "constants/event-tracker"; +import { EUserWorkspaceRoles, ROLE } from "constants/workspace"; +// hooks +import { useEventTracker, useMember, useUser } from "hooks/store"; type Props = { memberId: string; diff --git a/web/components/workspace/settings/members-list.tsx b/web/components/workspace/settings/members-list.tsx index 1dc02d508..216122525 100644 --- a/web/components/workspace/settings/members-list.tsx +++ b/web/components/workspace/settings/members-list.tsx @@ -1,13 +1,12 @@ import { FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; +// components +import { MembersSettingsLoader } from "components/ui"; +import { WorkspaceInvitationsListItem, WorkspaceMembersListItem } from "components/workspace"; // hooks import { useMember } from "hooks/store"; -// components -import { WorkspaceInvitationsListItem, WorkspaceMembersListItem } from "components/workspace"; -// ui -import { MembersSettingsLoader } from "components/ui"; export const WorkspaceMembersList: FC<{ searchQuery: string }> = observer((props) => { const { searchQuery } = props; diff --git a/web/components/workspace/settings/workspace-details.tsx b/web/components/workspace/settings/workspace-details.tsx index d491ca08e..bfd1473ea 100644 --- a/web/components/workspace/settings/workspace-details.tsx +++ b/web/components/workspace/settings/workspace-details.tsx @@ -3,22 +3,22 @@ import { observer } from "mobx-react-lite"; import { Controller, useForm } from "react-hook-form"; import { Disclosure, Transition } from "@headlessui/react"; import { ChevronDown, ChevronUp, Pencil } from "lucide-react"; -// services -import { FileService } from "services/file.service"; -// hooks -import { useEventTracker, useUser, useWorkspace } from "hooks/store"; -// components -import { DeleteWorkspaceModal } from "components/workspace"; -import { WorkspaceImageUploadModal } from "components/core"; // ui import { Button, CustomSelect, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { WorkspaceImageUploadModal } from "components/core"; +import { DeleteWorkspaceModal } from "components/workspace"; +// constants +import { WORKSPACE_UPDATED } from "constants/event-tracker"; +import { EUserWorkspaceRoles, ORGANIZATION_SIZE } from "constants/workspace"; // helpers import { copyUrlToClipboard } from "helpers/string.helper"; +// hooks +import { useEventTracker, useUser, useWorkspace } from "hooks/store"; +// services +import { FileService } from "services/file.service"; // types import { IWorkspace } from "@plane/types"; -// constants -import { EUserWorkspaceRoles, ORGANIZATION_SIZE } from "constants/workspace"; -import { WORKSPACE_UPDATED } from "constants/event-tracker"; const defaultValues: Partial = { name: "", diff --git a/web/components/workspace/sidebar-dropdown.tsx b/web/components/workspace/sidebar-dropdown.tsx index 98a133ee3..5d1695b33 100644 --- a/web/components/workspace/sidebar-dropdown.tsx +++ b/web/components/workspace/sidebar-dropdown.tsx @@ -1,16 +1,18 @@ import { Fragment, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import Link from "next/link"; +import { useRouter } from "next/router"; import { useTheme } from "next-themes"; -import { Menu, Transition } from "@headlessui/react"; -import { mutate } from "swr"; -import { Check, ChevronDown, CircleUserRound, LogOut, Mails, PlusSquare, Settings, UserCircle2 } from "lucide-react"; import { usePopper } from "react-popper"; +import { mutate } from "swr"; +// ui +import { Menu, Transition } from "@headlessui/react"; +// icons +import { Check, ChevronDown, CircleUserRound, LogOut, Mails, PlusSquare, Settings, UserCircle2 } from "lucide-react"; +// plane ui +import { Avatar, Loader, TOAST_TYPE, setToast } from "@plane/ui"; // hooks import { useApplication, useUser, useWorkspace } from "hooks/store"; -// ui -import { Avatar, Loader, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IWorkspace } from "@plane/types"; // Static Data diff --git a/web/components/workspace/sidebar-menu.tsx b/web/components/workspace/sidebar-menu.tsx index 774a231db..2069d8f27 100644 --- a/web/components/workspace/sidebar-menu.tsx +++ b/web/components/workspace/sidebar-menu.tsx @@ -1,20 +1,20 @@ import React from "react"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; -// hooks -import { useApplication, useEventTracker, useUser } from "hooks/store"; -// components -import { NotificationPopover } from "components/notifications"; +import { Crown } from "lucide-react"; // ui import { Tooltip } from "@plane/ui"; -import { Crown } from "lucide-react"; +// components +import { NotificationPopover } from "components/notifications"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; import { SIDEBAR_MENU_ITEMS } from "constants/dashboard"; import { SIDEBAR_CLICKED } from "constants/event-tracker"; +import { EUserWorkspaceRoles } from "constants/workspace"; // helper import { cn } from "helpers/common.helper"; +// hooks +import { useApplication, useEventTracker, useUser } from "hooks/store"; export const WorkspaceSidebarMenu = observer(() => { // store hooks diff --git a/web/components/workspace/sidebar-quick-action.tsx b/web/components/workspace/sidebar-quick-action.tsx index dd2dd5c68..d2ce2f5b3 100644 --- a/web/components/workspace/sidebar-quick-action.tsx +++ b/web/components/workspace/sidebar-quick-action.tsx @@ -1,14 +1,14 @@ import { useRef, useState } from "react"; import { observer } from "mobx-react-lite"; import { ChevronUp, PenSquare, Search } from "lucide-react"; -// hooks -import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; -import useLocalStorage from "hooks/use-local-storage"; // components import { CreateUpdateIssueModal } from "components/issues"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; import { EIssuesStoreType } from "constants/issue"; +import { EUserWorkspaceRoles } from "constants/workspace"; +// hooks +import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; +import useLocalStorage from "hooks/use-local-storage"; // types import { TIssue } from "@plane/types"; @@ -27,11 +27,12 @@ export const WorkspaceSidebarQuickAction = observer(() => { membership: { currentWorkspaceRole }, } = useUser(); - const { storedValue, setValue } = useLocalStorage>>("draftedIssue", {}); + const { storedValue } = useLocalStorage>>("draftedIssue", {}); //useState control for displaying draft issue button instead of group hover const [isDraftButtonOpen, setIsDraftButtonOpen] = useState(false); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const timeoutRef = useRef(); const isSidebarCollapsed = themeStore.sidebarCollapsed; @@ -41,7 +42,7 @@ export const WorkspaceSidebarQuickAction = observer(() => { const disabled = joinedProjectIds.length === 0; const onMouseEnter = () => { - //if renet before timout clear the timeout + // if enter before time out clear the timeout timeoutRef?.current && clearTimeout(timeoutRef.current); setIsDraftButtonOpen(true); }; @@ -68,7 +69,7 @@ export const WorkspaceSidebarQuickAction = observer(() => { onClose={() => setIsDraftIssueModalOpen(false)} data={workspaceDraftIssue ?? {}} onSubmit={() => removeWorkspaceDraftIssue()} - isDraft={true} + isDraft />
) => Promise; @@ -200,8 +200,8 @@ export const WorkspaceViewForm: React.FC = observer((props) => { ? "Updating View..." : "Update View" : isSubmitting - ? "Creating View..." - : "Create View"} + ? "Creating View..." + : "Create View"}
diff --git a/web/components/workspace/views/header.tsx b/web/components/workspace/views/header.tsx index 223fda13c..97982e61e 100644 --- a/web/components/workspace/views/header.tsx +++ b/web/components/workspace/views/header.tsx @@ -1,15 +1,16 @@ import React, { useEffect, useRef, useState } from "react"; -import { useRouter } from "next/router"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; +import { useRouter } from "next/router"; +// icons import { Plus } from "lucide-react"; -// store hooks -import { useEventTracker, useGlobalView, useUser } from "hooks/store"; // components import { CreateUpdateWorkspaceViewModal } from "components/workspace"; // constants -import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "constants/workspace"; import { GLOBAL_VIEW_OPENED } from "constants/event-tracker"; +import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "constants/workspace"; +// store hooks +import { useEventTracker, useGlobalView, useUser } from "hooks/store"; const ViewTab = observer((props: { viewId: string }) => { const { viewId } = props; @@ -69,7 +70,7 @@ export const GlobalViewsHeader: React.FC = observer(() => { activeTabElement.scrollIntoView({ behavior: "smooth", inline: diff > 500 ? "center" : "nearest" }); } } - }, [globalViewId, currentWorkspaceViews, containerRef]); + }, [globalViewId, currentWorkspaceViews, containerRef, captureEvent]); const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; @@ -95,9 +96,7 @@ export const GlobalViewsHeader: React.FC = observer(() => { ))} - {currentWorkspaceViews?.map((viewId) => ( - - ))} + {currentWorkspaceViews?.map((viewId) => )}
{isAuthorizedUser && ( diff --git a/web/components/workspace/views/modal.tsx b/web/components/workspace/views/modal.tsx index 6543a8321..975018f16 100644 --- a/web/components/workspace/views/modal.tsx +++ b/web/components/workspace/views/modal.tsx @@ -1,17 +1,17 @@ import React from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; -// store hooks -import { useEventTracker, useGlobalView } from "hooks/store"; // ui import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { WorkspaceViewForm } from "components/workspace"; -// types -import { IWorkspaceView } from "@plane/types"; // constants import { GLOBAL_VIEW_CREATED, GLOBAL_VIEW_UPDATED } from "constants/event-tracker"; +// store hooks +import { useEventTracker, useGlobalView } from "hooks/store"; +// types +import { IWorkspaceView } from "@plane/types"; type Props = { data?: IWorkspaceView; diff --git a/web/components/workspace/views/view-list-item.tsx b/web/components/workspace/views/view-list-item.tsx index 28f25551c..4030dc181 100644 --- a/web/components/workspace/views/view-list-item.tsx +++ b/web/components/workspace/views/view-list-item.tsx @@ -1,17 +1,18 @@ import { useState } from "react"; -import { useRouter } from "next/router"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; +import { useRouter } from "next/router"; +// icons import { Pencil, Trash2 } from "lucide-react"; -// store hooks -import { useEventTracker, useGlobalView } from "hooks/store"; -// components -import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "components/workspace"; // ui import { CustomMenu } from "@plane/ui"; +// components +import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "components/workspace"; // helpers -import { truncateText } from "helpers/string.helper"; import { calculateTotalFilters } from "helpers/filter.helper"; +import { truncateText } from "helpers/string.helper"; +// store hooks +import { useEventTracker, useGlobalView } from "hooks/store"; type Props = { viewId: string }; diff --git a/web/components/workspace/views/views-list.tsx b/web/components/workspace/views/views-list.tsx index 9a8758d2d..ef33fe16e 100644 --- a/web/components/workspace/views/views-list.tsx +++ b/web/components/workspace/views/views-list.tsx @@ -1,12 +1,11 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; +// components +import { ViewListLoader } from "components/ui"; +import { GlobalViewListItem } from "components/workspace"; // store hooks import { useGlobalView } from "hooks/store"; -// components -import { GlobalViewListItem } from "components/workspace"; -// ui -import { ViewListLoader } from "components/ui"; type Props = { searchQuery: string; @@ -29,11 +28,5 @@ export const GlobalViewsList: React.FC = observer((props) => { const filteredViewsList = getSearchedViews(searchQuery); - return ( - <> - {filteredViewsList?.map((viewId) => ( - - ))} - - ); + return <>{filteredViewsList?.map((viewId) => )}; }); diff --git a/web/components/workspace/workspace-active-cycles-upgrade.tsx b/web/components/workspace/workspace-active-cycles-upgrade.tsx index b5a61610b..23ab27acf 100644 --- a/web/components/workspace/workspace-active-cycles-upgrade.tsx +++ b/web/components/workspace/workspace-active-cycles-upgrade.tsx @@ -1,16 +1,16 @@ import React from "react"; -import Image from "next/image"; import { observer } from "mobx-react"; -// hooks -import { useUser } from "hooks/store"; -// ui -import { getButtonStyling } from "@plane/ui"; +import Image from "next/image"; // icons import { Crown } from "lucide-react"; -// helper -import { cn } from "helpers/common.helper"; +// ui +import { getButtonStyling } from "@plane/ui"; // constants import { WORKSPACE_ACTIVE_CYCLES_DETAILS } from "constants/cycle"; +// helper +import { cn } from "helpers/common.helper"; +// hooks +import { useUser } from "hooks/store"; export const WorkspaceActiveCyclesUpgrade = observer(() => { // store hooks @@ -75,7 +75,7 @@ export const WorkspaceActiveCyclesUpgrade = observer(() => {
{WORKSPACE_ACTIVE_CYCLES_DETAILS.map((item) => ( -
+

{item.title}

diff --git a/web/constants/cycle.ts b/web/constants/cycle.ts index 63900b6b7..8bb43d898 100644 --- a/web/constants/cycle.ts +++ b/web/constants/cycle.ts @@ -162,5 +162,3 @@ export const WORKSPACE_ACTIVE_CYCLES_DETAILS = [ icon: Microscope, }, ]; - - diff --git a/web/constants/dashboard.ts b/web/constants/dashboard.ts index 6ac4e7817..a3f5f7e00 100644 --- a/web/constants/dashboard.ts +++ b/web/constants/dashboard.ts @@ -1,19 +1,20 @@ import { linearGradientDef } from "@nivo/core"; // assets -import UpcomingIssuesDark from "public/empty-state/dashboard/dark/upcoming-issues.svg"; -import UpcomingIssuesLight from "public/empty-state/dashboard/light/upcoming-issues.svg"; -import OverdueIssuesDark from "public/empty-state/dashboard/dark/overdue-issues.svg"; -import OverdueIssuesLight from "public/empty-state/dashboard/light/overdue-issues.svg"; +import { BarChart2, Briefcase, CheckCircle, LayoutGrid } from "lucide-react"; +import { ContrastIcon } from "@plane/ui"; +import { Props } from "components/icons/types"; import CompletedIssuesDark from "public/empty-state/dashboard/dark/completed-issues.svg"; +import OverdueIssuesDark from "public/empty-state/dashboard/dark/overdue-issues.svg"; +import UpcomingIssuesDark from "public/empty-state/dashboard/dark/upcoming-issues.svg"; import CompletedIssuesLight from "public/empty-state/dashboard/light/completed-issues.svg"; +import OverdueIssuesLight from "public/empty-state/dashboard/light/overdue-issues.svg"; +import UpcomingIssuesLight from "public/empty-state/dashboard/light/upcoming-issues.svg"; // types import { EDurationFilters, TIssuesListTypes, TStateGroups } from "@plane/types"; import { Props } from "components/icons/types"; // constants import { EUserWorkspaceRoles } from "./workspace"; // icons -import { BarChart2, Briefcase, CheckCircle, LayoutGrid } from "lucide-react"; -import { ContrastIcon } from "@plane/ui"; // gradients for issues by priority widget graph bars export const PRIORITY_GRAPH_GRADIENTS = [ diff --git a/web/constants/event-tracker.ts b/web/constants/event-tracker.ts index 37a18a37d..7edfccba5 100644 --- a/web/constants/event-tracker.ts +++ b/web/constants/event-tracker.ts @@ -112,16 +112,16 @@ export const getIssueEventPayload = (props: IssueEventProps) => { updated_from: props.path?.includes("workspace-views") ? "All views" : props.path?.includes("cycles") - ? "Cycle" - : props.path?.includes("modules") - ? "Module" - : props.path?.includes("views") - ? "Project view" - : props.path?.includes("inbox") - ? "Inbox" - : props.path?.includes("draft") - ? "Draft" - : "Project", + ? "Cycle" + : props.path?.includes("modules") + ? "Module" + : props.path?.includes("views") + ? "Project view" + : props.path?.includes("inbox") + ? "Inbox" + : props.path?.includes("draft") + ? "Draft" + : "Project", }; } return eventPayload; diff --git a/web/constants/spreadsheet.ts b/web/constants/spreadsheet.ts index aa588d9e1..50d6c15df 100644 --- a/web/constants/spreadsheet.ts +++ b/web/constants/spreadsheet.ts @@ -1,8 +1,7 @@ -import { IIssueDisplayProperties, TIssue, TIssueOrderByOptions } from "@plane/types"; -import { LayersIcon, DoubleCircleIcon, UserGroupIcon, DiceIcon, ContrastIcon } from "@plane/ui"; -import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip, CalendarCheck2, CalendarClock } from "lucide-react"; import { FC } from "react"; import { ISvgIcons } from "@plane/ui/src/icons/type"; +import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip, CalendarCheck2, CalendarClock } from "lucide-react"; +import { LayersIcon, DoubleCircleIcon, UserGroupIcon, DiceIcon, ContrastIcon } from "@plane/ui"; import { SpreadsheetAssigneeColumn, SpreadsheetAttachmentColumn, @@ -19,6 +18,7 @@ import { SpreadsheetSubIssueColumn, SpreadsheetUpdatedOnColumn, } from "components/issues/issue-layouts/spreadsheet"; +import { IIssueDisplayProperties, TIssue, TIssueOrderByOptions } from "@plane/types"; export const SPREADSHEET_PROPERTY_DETAILS: { [key: string]: { diff --git a/web/constants/workspace.ts b/web/constants/workspace.ts index 1471de395..7ae89d5d6 100644 --- a/web/constants/workspace.ts +++ b/web/constants/workspace.ts @@ -1,14 +1,14 @@ // services images -import GithubLogo from "public/services/github.png"; -import JiraLogo from "public/services/jira.svg"; +import { SettingIcon } from "components/icons"; +import { Props } from "components/icons/types"; import CSVLogo from "public/services/csv.svg"; import ExcelLogo from "public/services/excel.svg"; +import GithubLogo from "public/services/github.png"; +import JiraLogo from "public/services/jira.svg"; import JSONLogo from "public/services/json.svg"; // types import { TStaticViewTypes } from "@plane/types"; -import { Props } from "components/icons/types"; // icons -import { SettingIcon } from "components/icons"; export enum EUserWorkspaceRoles { GUEST = 5, diff --git a/web/contexts/user-notification-context.tsx b/web/contexts/user-notification-context.tsx index b55a05771..ef3af2124 100644 --- a/web/contexts/user-notification-context.tsx +++ b/web/contexts/user-notification-context.tsx @@ -3,9 +3,9 @@ import { createContext, useCallback, useEffect, useReducer } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; // services +import { UNREAD_NOTIFICATIONS_COUNT, USER_WORKSPACE_NOTIFICATIONS } from "constants/fetch-keys"; import { NotificationService } from "services/notification.service"; // fetch-keys -import { UNREAD_NOTIFICATIONS_COUNT, USER_WORKSPACE_NOTIFICATIONS } from "constants/fetch-keys"; // type import type { NotificationType, NotificationCount, IUserNotification } from "@plane/types"; diff --git a/web/helpers/analytics.helper.ts b/web/helpers/analytics.helper.ts index 58a456ed7..dfa98d7ea 100644 --- a/web/helpers/analytics.helper.ts +++ b/web/helpers/analytics.helper.ts @@ -1,13 +1,13 @@ // nivo import { BarDatum } from "@nivo/bar"; // helpers +import { DATE_KEYS } from "constants/analytics"; +import { MONTHS_LIST } from "constants/calendar"; +import { STATE_GROUPS } from "constants/state"; import { addSpaceIfCamelCase, capitalizeFirstLetter, generateRandomColor } from "helpers/string.helper"; // types import { IAnalyticsData, IAnalyticsParams, IAnalyticsResponse, TStateGroups } from "@plane/types"; // constants -import { STATE_GROUPS } from "constants/state"; -import { MONTHS_LIST } from "constants/calendar"; -import { DATE_KEYS } from "constants/analytics"; export const convertResponseToBarGraphData = ( response: IAnalyticsData | undefined, @@ -36,8 +36,8 @@ export const convertResponseToBarGraphData = ( name: DATE_KEYS.includes(params.x_axis) ? renderMonthAndYear(key) : params.x_axis === "priority" || params.x_axis === "state__group" - ? capitalizeFirstLetter(key) - : key, + ? capitalizeFirstLetter(key) + : key, ...segments, }); } else { @@ -49,8 +49,8 @@ export const convertResponseToBarGraphData = ( name: DATE_KEYS.includes(params.x_axis) ? renderMonthAndYear(item.dimension) : params.x_axis === "priority" || params.x_axis === "state__group" - ? capitalizeFirstLetter(item.dimension ?? "None") - : item.dimension ?? "None", + ? capitalizeFirstLetter(item.dimension ?? "None") + : item.dimension ?? "None", [yAxisKey]: item[yAxisKey] ?? 0, }); } @@ -84,12 +84,12 @@ export const generateBarColor = ( priority === "urgent" ? "#ef4444" : priority === "high" - ? "#f97316" - : priority === "medium" - ? "#eab308" - : priority === "low" - ? "#22c55e" - : "#ced4da"; + ? "#f97316" + : priority === "medium" + ? "#eab308" + : priority === "low" + ? "#22c55e" + : "#ced4da"; } return color ?? generateRandomColor(value); diff --git a/web/helpers/calendar.helper.ts b/web/helpers/calendar.helper.ts index e570a5c9a..6c648dd6b 100644 --- a/web/helpers/calendar.helper.ts +++ b/web/helpers/calendar.helper.ts @@ -1,7 +1,7 @@ // helpers +import { ICalendarDate, ICalendarPayload } from "components/issues"; import { getWeekNumberOfDate, renderFormattedPayloadDate } from "helpers/date-time.helper"; // types -import { ICalendarDate, ICalendarPayload } from "components/issues"; export const formatDate = (date: Date, format: string): string => { const day = date.getDate(); diff --git a/web/helpers/filter.helper.ts b/web/helpers/filter.helper.ts index 0b30f95e1..d31a25b3d 100644 --- a/web/helpers/filter.helper.ts +++ b/web/helpers/filter.helper.ts @@ -13,4 +13,3 @@ export const calculateTotalFilters = (filters: IIssueFilterOptions): number => ) .reduce((curr, prev) => curr + prev, 0) : 0; - diff --git a/web/helpers/issue.helper.ts b/web/helpers/issue.helper.ts index 831cb321e..08cb4abd7 100644 --- a/web/helpers/issue.helper.ts +++ b/web/helpers/issue.helper.ts @@ -1,8 +1,12 @@ -import { v4 as uuidv4 } from "uuid"; import differenceInCalendarDays from "date-fns/differenceInCalendarDays"; +import { v4 as uuidv4 } from "uuid"; // helpers -import { orderArrayBy } from "helpers/array.helper"; // types +import { IGanttBlock } from "components/gantt-chart"; +// constants +import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { STATE_GROUPS } from "constants/state"; +import { orderArrayBy } from "helpers/array.helper"; import { TIssue, TIssueGroupByOptions, @@ -11,10 +15,6 @@ import { TIssueParams, TStateGroups, } from "@plane/types"; -import { IGanttBlock } from "components/gantt-chart"; -// constants -import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; -import { STATE_GROUPS } from "constants/state"; type THandleIssuesMutation = ( formData: Partial, diff --git a/web/helpers/string.helper.ts b/web/helpers/string.helper.ts index d30b29b52..ad87c2e75 100644 --- a/web/helpers/string.helper.ts +++ b/web/helpers/string.helper.ts @@ -1,10 +1,10 @@ +import * as DOMPurify from "dompurify"; import { CYCLE_ISSUES_WITH_PARAMS, MODULE_ISSUES_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS, VIEW_ISSUES, } from "constants/fetch-keys"; -import * as DOMPurify from 'dompurify'; export const addSpaceIfCamelCase = (str: string) => { if (str === undefined || str === null) return ""; @@ -172,10 +172,10 @@ export const getFetchKeysForIssueMutation = (options: { const ganttFetchKey = cycleId ? { ganttFetchKey: CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), ganttParams) } : moduleId - ? { ganttFetchKey: MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), ganttParams) } - : viewId - ? { ganttFetchKey: VIEW_ISSUES(viewId.toString(), viewGanttParams) } - : { ganttFetchKey: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", ganttParams) }; + ? { ganttFetchKey: MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), ganttParams) } + : viewId + ? { ganttFetchKey: VIEW_ISSUES(viewId.toString(), viewGanttParams) } + : { ganttFetchKey: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", ganttParams) }; return { ...ganttFetchKey, diff --git a/web/hooks/store/index.ts b/web/hooks/store/index.ts index 2349b1585..ff036a529 100644 --- a/web/hooks/store/index.ts +++ b/web/hooks/store/index.ts @@ -1,5 +1,5 @@ export * from "./use-application"; -export * from "./use-event-tracker" +export * from "./use-event-tracker"; export * from "./use-calendar-view"; export * from "./use-cycle"; export * from "./use-dashboard"; diff --git a/web/hooks/store/use-inbox-issues.ts b/web/hooks/store/use-inbox-issues.ts index 2b2941f84..1196eae90 100644 --- a/web/hooks/store/use-inbox-issues.ts +++ b/web/hooks/store/use-inbox-issues.ts @@ -2,8 +2,8 @@ import { useContext } from "react"; // mobx store import { StoreContext } from "contexts/store-context"; // types -import { IInboxIssue } from "store/inbox/inbox_issue.store"; import { IInboxFilter } from "store/inbox/inbox_filter.store"; +import { IInboxIssue } from "store/inbox/inbox_issue.store"; export const useInboxIssues = (): { issues: IInboxIssue; diff --git a/web/hooks/store/use-issues.ts b/web/hooks/store/use-issues.ts index f2da9d954..ed270c9ec 100644 --- a/web/hooks/store/use-issues.ts +++ b/web/hooks/store/use-issues.ts @@ -1,19 +1,19 @@ import { useContext } from "react"; import merge from "lodash/merge"; // mobx store +import { EIssuesStoreType } from "constants/issue"; import { StoreContext } from "contexts/store-context"; // types -import { IWorkspaceIssues, IWorkspaceIssuesFilter } from "store/issue/workspace"; +import { IArchivedIssues, IArchivedIssuesFilter } from "store/issue/archived"; +import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle"; +import { IDraftIssues, IDraftIssuesFilter } from "store/issue/draft"; +import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module"; import { IProfileIssues, IProfileIssuesFilter } from "store/issue/profile"; import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project"; -import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle"; -import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module"; import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views"; -import { IArchivedIssues, IArchivedIssuesFilter } from "store/issue/archived"; -import { IDraftIssues, IDraftIssuesFilter } from "store/issue/draft"; +import { IWorkspaceIssues, IWorkspaceIssuesFilter } from "store/issue/workspace"; import { TIssueMap } from "@plane/types"; // constants -import { EIssuesStoreType } from "constants/issue"; type defaultIssueStore = { issueMap: TIssueMap; diff --git a/web/hooks/use-comment-reaction.tsx b/web/hooks/use-comment-reaction.tsx index 2327fddcd..3750160b0 100644 --- a/web/hooks/use-comment-reaction.tsx +++ b/web/hooks/use-comment-reaction.tsx @@ -2,9 +2,9 @@ import useSWR from "swr"; // fetch keys import { COMMENT_REACTION_LIST } from "constants/fetch-keys"; // services +import { groupReactions } from "helpers/emoji.helper"; import { IssueReactionService } from "services/issue"; // helpers -import { groupReactions } from "helpers/emoji.helper"; import { useUser } from "./store"; // hooks diff --git a/web/hooks/use-draggable-portal.ts b/web/hooks/use-draggable-portal.ts index 383c277f3..325f8b268 100644 --- a/web/hooks/use-draggable-portal.ts +++ b/web/hooks/use-draggable-portal.ts @@ -1,6 +1,6 @@ -import { createPortal } from "react-dom"; import { useEffect, useRef } from "react"; import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; +import { createPortal } from "react-dom"; const useDraggableInPortal = () => { const self = useRef(); diff --git a/web/hooks/use-dropdown-key-down.tsx b/web/hooks/use-dropdown-key-down.tsx index 228e35575..174cfdd8a 100644 --- a/web/hooks/use-dropdown-key-down.tsx +++ b/web/hooks/use-dropdown-key-down.tsx @@ -1,9 +1,11 @@ import { useCallback } from "react"; type TUseDropdownKeyDown = { - (onEnterKeyDown: () => void, onEscKeyDown: () => void, stopPropagation?: boolean): ( - event: React.KeyboardEvent - ) => void; + ( + onEnterKeyDown: () => void, + onEscKeyDown: () => void, + stopPropagation?: boolean + ): (event: React.KeyboardEvent) => void; }; export const useDropdownKeyDown: TUseDropdownKeyDown = (onEnterKeyDown, onEscKeyDown, stopPropagation = true) => { diff --git a/web/hooks/use-user-notifications.tsx b/web/hooks/use-user-notifications.tsx index 3c2ec6332..41bb6cbfd 100644 --- a/web/hooks/use-user-notifications.tsx +++ b/web/hooks/use-user-notifications.tsx @@ -4,9 +4,9 @@ import { useRouter } from "next/router"; import useSWR from "swr"; import useSWRInfinite from "swr/infinite"; // services +import { UNREAD_NOTIFICATIONS_COUNT, getPaginatedNotificationKey } from "constants/fetch-keys"; import { NotificationService } from "services/notification.service"; // fetch-keys -import { UNREAD_NOTIFICATIONS_COUNT, getPaginatedNotificationKey } from "constants/fetch-keys"; // type import type { NotificationType, NotificationCount, IMarkAllAsReadPayload } from "@plane/types"; // ui diff --git a/web/hooks/use-user.tsx b/web/hooks/use-user.tsx index 357579026..ffe6c963b 100644 --- a/web/hooks/use-user.tsx +++ b/web/hooks/use-user.tsx @@ -2,9 +2,9 @@ import { useEffect } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; // services +import { CURRENT_USER } from "constants/fetch-keys"; import { UserService } from "services/user.service"; // constants -import { CURRENT_USER } from "constants/fetch-keys"; // types import type { IUser } from "@plane/types"; diff --git a/web/layouts/admin-layout/header.tsx b/web/layouts/admin-layout/header.tsx index 2607fe91d..e12875d86 100644 --- a/web/layouts/admin-layout/header.tsx +++ b/web/layouts/admin-layout/header.tsx @@ -2,9 +2,9 @@ import { FC } from "react"; // mobx import { observer } from "mobx-react-lite"; // ui +import { Settings } from "lucide-react"; import { Breadcrumbs } from "@plane/ui"; // icons -import { Settings } from "lucide-react"; import { BreadcrumbLink } from "components/common"; export interface IInstanceAdminHeader { diff --git a/web/layouts/admin-layout/layout.tsx b/web/layouts/admin-layout/layout.tsx index 2dbcdf1f5..bd53fc060 100644 --- a/web/layouts/admin-layout/layout.tsx +++ b/web/layouts/admin-layout/layout.tsx @@ -1,13 +1,13 @@ import { FC, ReactNode } from "react"; import { observer } from "mobx-react-lite"; // hooks +import { InstanceSetupView } from "components/instance"; import { useApplication } from "hooks/store"; // layouts import { AdminAuthWrapper, UserAuthWrapper } from "layouts/auth-layout"; // components -import { InstanceAdminSidebar } from "./sidebar"; import { InstanceAdminHeader } from "./header"; -import { InstanceSetupView } from "components/instance"; +import { InstanceAdminSidebar } from "./sidebar"; export interface IInstanceAdminLayout { children: ReactNode; diff --git a/web/layouts/admin-layout/sidebar.tsx b/web/layouts/admin-layout/sidebar.tsx index efd3cfc76..2af3f0982 100644 --- a/web/layouts/admin-layout/sidebar.tsx +++ b/web/layouts/admin-layout/sidebar.tsx @@ -1,9 +1,9 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // hooks +import { InstanceAdminSidebarMenu, InstanceHelpSection, InstanceSidebarDropdown } from "components/instance"; import { useApplication } from "hooks/store"; // components -import { InstanceAdminSidebarMenu, InstanceHelpSection, InstanceSidebarDropdown } from "components/instance"; export interface IInstanceAdminSidebar {} diff --git a/web/layouts/app-layout/layout.tsx b/web/layouts/app-layout/layout.tsx index 07ec9711d..dd1df164f 100644 --- a/web/layouts/app-layout/layout.tsx +++ b/web/layouts/app-layout/layout.tsx @@ -1,10 +1,13 @@ import { FC, ReactNode } from "react"; // layouts +import { observer } from "mobx-react-lite"; +import useSWR from "swr"; +import { CommandPalette } from "components/command-palette"; +import { EIssuesStoreType } from "constants/issue"; +import { useIssues } from "hooks/store/use-issues"; import { UserAuthWrapper, WorkspaceAuthWrapper, ProjectAuthWrapper } from "layouts/auth-layout"; // components -import { CommandPalette } from "components/command-palette"; import { AppSidebar } from "./sidebar"; -import { observer } from "mobx-react-lite"; export interface IAppLayout { children: ReactNode; diff --git a/web/layouts/app-layout/sidebar.tsx b/web/layouts/app-layout/sidebar.tsx index c3b47d021..6ff6f01d7 100644 --- a/web/layouts/app-layout/sidebar.tsx +++ b/web/layouts/app-layout/sidebar.tsx @@ -1,13 +1,13 @@ import { FC, useRef } from "react"; import { observer } from "mobx-react-lite"; // components +import { ProjectSidebarList } from "components/project"; import { WorkspaceHelpSection, WorkspaceSidebarDropdown, WorkspaceSidebarMenu, WorkspaceSidebarQuickAction, } from "components/workspace"; -import { ProjectSidebarList } from "components/project"; // hooks import { useApplication } from "hooks/store"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; diff --git a/web/layouts/auth-layout/admin-wrapper.tsx b/web/layouts/auth-layout/admin-wrapper.tsx index 236d1c440..6d44e6f14 100644 --- a/web/layouts/auth-layout/admin-wrapper.tsx +++ b/web/layouts/auth-layout/admin-wrapper.tsx @@ -1,9 +1,9 @@ import { FC, ReactNode } from "react"; import { observer } from "mobx-react-lite"; // hooks +import { InstanceAdminRestriction } from "components/instance"; import { useApplication, useUser } from "hooks/store"; // components -import { InstanceAdminRestriction } from "components/instance"; export interface IAdminAuthWrapper { children: ReactNode; diff --git a/web/layouts/auth-layout/project-wrapper.tsx b/web/layouts/auth-layout/project-wrapper.tsx index bdd2da8b5..fc672c812 100644 --- a/web/layouts/auth-layout/project-wrapper.tsx +++ b/web/layouts/auth-layout/project-wrapper.tsx @@ -1,8 +1,12 @@ import { FC, ReactNode } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; // hooks +// components +import { Spinner } from "@plane/ui"; +import { JoinProject } from "components/auth-screens"; +import { EmptyState } from "components/common"; import { useApplication, useEventTracker, @@ -17,10 +21,6 @@ import { useUser, useInbox, } from "hooks/store"; -// components -import { Spinner } from "@plane/ui"; -import { JoinProject } from "components/auth-screens"; -import { EmptyState } from "components/common"; // images import emptyProject from "public/empty-state/project.svg"; diff --git a/web/layouts/auth-layout/user-wrapper.tsx b/web/layouts/auth-layout/user-wrapper.tsx index b48e20b10..2a9502a0b 100644 --- a/web/layouts/auth-layout/user-wrapper.tsx +++ b/web/layouts/auth-layout/user-wrapper.tsx @@ -1,12 +1,12 @@ import { FC, ReactNode } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; import useSWRImmutable from "swr/immutable"; // hooks +import { Spinner } from "@plane/ui"; import { useUser, useWorkspace } from "hooks/store"; // ui -import { Spinner } from "@plane/ui"; export interface IUserAuthWrapper { children: ReactNode; diff --git a/web/layouts/auth-layout/workspace-wrapper.tsx b/web/layouts/auth-layout/workspace-wrapper.tsx index ba6498301..199a7e5bc 100644 --- a/web/layouts/auth-layout/workspace-wrapper.tsx +++ b/web/layouts/auth-layout/workspace-wrapper.tsx @@ -1,12 +1,12 @@ import { FC, ReactNode } from "react"; -import { useRouter } from "next/router"; -import Link from "next/link"; -import useSWR from "swr"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import useSWR from "swr"; // hooks +import { Button, Spinner } from "@plane/ui"; import { useLabel, useMember, useProject, useUser } from "hooks/store"; // icons -import { Button, Spinner } from "@plane/ui"; export interface IWorkspaceAuthWrapper { children: ReactNode; diff --git a/web/layouts/instance-layout/index.tsx b/web/layouts/instance-layout/index.tsx index d5df476d9..7e22b7321 100644 --- a/web/layouts/instance-layout/index.tsx +++ b/web/layouts/instance-layout/index.tsx @@ -1,12 +1,12 @@ import { FC, ReactNode } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react-lite"; // hooks -import { useApplication } from "hooks/store"; -// components import { Spinner } from "@plane/ui"; import { InstanceNotReady } from "components/instance"; +import { useApplication } from "hooks/store"; +// components type Props = { children: ReactNode; diff --git a/web/layouts/settings-layout/profile/layout.tsx b/web/layouts/settings-layout/profile/layout.tsx index 5bf5f0eea..ed594c9f2 100644 --- a/web/layouts/settings-layout/profile/layout.tsx +++ b/web/layouts/settings-layout/profile/layout.tsx @@ -1,9 +1,9 @@ import { FC, ReactNode } from "react"; // layout +import { CommandPalette } from "components/command-palette"; import { UserAuthWrapper } from "layouts/auth-layout"; import { ProfileLayoutSidebar } from "layouts/settings-layout"; // components -import { CommandPalette } from "components/command-palette"; interface IProfileSettingsLayout { children: ReactNode; diff --git a/web/layouts/settings-layout/profile/preferences/index.ts b/web/layouts/settings-layout/profile/preferences/index.ts index 34e230258..c4bfd4db3 100644 --- a/web/layouts/settings-layout/profile/preferences/index.ts +++ b/web/layouts/settings-layout/profile/preferences/index.ts @@ -1,2 +1,2 @@ export * from "./layout"; -export * from "./sidebar"; \ No newline at end of file +export * from "./sidebar"; diff --git a/web/layouts/settings-layout/profile/preferences/layout.tsx b/web/layouts/settings-layout/profile/preferences/layout.tsx index 116813958..71a1fdd85 100644 --- a/web/layouts/settings-layout/profile/preferences/layout.tsx +++ b/web/layouts/settings-layout/profile/preferences/layout.tsx @@ -1,13 +1,13 @@ import { FC, ReactNode } from "react"; // layout -import { ProfileSettingsLayout } from "layouts/settings-layout"; -import { ProfilePreferenceSettingsSidebar } from "./sidebar"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { CustomMenu } from "@plane/ui"; -import { ChevronDown } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/router"; +import { ChevronDown } from "lucide-react"; +import { CustomMenu } from "@plane/ui"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { useApplication } from "hooks/store"; +import { ProfileSettingsLayout } from "layouts/settings-layout"; +import { ProfilePreferenceSettingsSidebar } from "./sidebar"; interface IProfilePreferenceSettingsLayout { children: ReactNode; diff --git a/web/layouts/settings-layout/profile/preferences/sidebar.tsx b/web/layouts/settings-layout/profile/preferences/sidebar.tsx index 7f43f3cad..27b28905b 100644 --- a/web/layouts/settings-layout/profile/preferences/sidebar.tsx +++ b/web/layouts/settings-layout/profile/preferences/sidebar.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { useRouter } from "next/router"; import Link from "next/link"; +import { useRouter } from "next/router"; export const ProfilePreferenceSettingsSidebar = () => { const router = useRouter(); @@ -9,15 +9,15 @@ export const ProfilePreferenceSettingsSidebar = () => { label: string; href: string; }> = [ - { - label: "Theme", - href: `/profile/preferences/theme`, - }, - { - label: "Email", - href: `/profile/preferences/email`, - }, - ]; + { + label: "Theme", + href: `/profile/preferences/theme`, + }, + { + label: "Email", + href: `/profile/preferences/email`, + }, + ]; return (
@@ -26,10 +26,11 @@ export const ProfilePreferenceSettingsSidebar = () => { {profilePreferenceLinks.map((link) => (
{link.label}
diff --git a/web/layouts/settings-layout/profile/sidebar.tsx b/web/layouts/settings-layout/profile/sidebar.tsx index 4d78195f1..caa5cd56e 100644 --- a/web/layouts/settings-layout/profile/sidebar.tsx +++ b/web/layouts/settings-layout/profile/sidebar.tsx @@ -1,9 +1,9 @@ import { useEffect, useRef, useState } from "react"; -import { mutate } from "swr"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import { useTheme } from "next-themes"; +import { mutate } from "swr"; import { ChevronLeft, LogOut, MoveLeft, Plus, UserPlus } from "lucide-react"; // hooks import { useApplication, useUser, useWorkspace } from "hooks/store"; @@ -11,7 +11,9 @@ import { useApplication, useUser, useWorkspace } from "hooks/store"; import { Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { PROFILE_ACTION_LINKS } from "constants/profile"; +import { useApplication, useUser, useWorkspace } from "hooks/store"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; +import useToast from "hooks/use-toast"; const WORKSPACE_ACTION_LINKS = [ { diff --git a/web/layouts/settings-layout/project/layout.tsx b/web/layouts/settings-layout/project/layout.tsx index 38525e98c..1ea4c2322 100644 --- a/web/layouts/settings-layout/project/layout.tsx +++ b/web/layouts/settings-layout/project/layout.tsx @@ -1,16 +1,16 @@ import { FC, ReactNode } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import Link from "next/link"; +import { useRouter } from "next/router"; // hooks -import { useUser } from "hooks/store"; // components -import { ProjectSettingsSidebar } from "./sidebar"; +import { Button, LayersIcon } from "@plane/ui"; import { NotAuthorizedView } from "components/auth-screens"; // ui -import { Button, LayersIcon } from "@plane/ui"; // constants import { EUserProjectRoles } from "constants/project"; +import { useUser } from "hooks/store"; +import { ProjectSettingsSidebar } from "./sidebar"; export interface IProjectSettingLayout { children: ReactNode; diff --git a/web/layouts/settings-layout/project/sidebar.tsx b/web/layouts/settings-layout/project/sidebar.tsx index 054add4ee..8cf2befc2 100644 --- a/web/layouts/settings-layout/project/sidebar.tsx +++ b/web/layouts/settings-layout/project/sidebar.tsx @@ -1,12 +1,12 @@ import React from "react"; -import { useRouter } from "next/router"; import Link from "next/link"; +import { useRouter } from "next/router"; // ui import { Loader } from "@plane/ui"; // hooks +import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "constants/project"; import { useUser } from "hooks/store"; // constants -import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "constants/project"; export const ProjectSettingsSidebar = () => { const router = useRouter(); diff --git a/web/layouts/settings-layout/workspace/sidebar.tsx b/web/layouts/settings-layout/workspace/sidebar.tsx index c8d4718c7..f5177139b 100644 --- a/web/layouts/settings-layout/workspace/sidebar.tsx +++ b/web/layouts/settings-layout/workspace/sidebar.tsx @@ -1,10 +1,10 @@ import React from "react"; -import { useRouter } from "next/router"; import Link from "next/link"; +import { useRouter } from "next/router"; // hooks +import { EUserWorkspaceRoles, WORKSPACE_SETTINGS_LINKS } from "constants/workspace"; import { useUser } from "hooks/store"; // constants -import { EUserWorkspaceRoles, WORKSPACE_SETTINGS_LINKS } from "constants/workspace"; export const WorkspaceSettingsSidebar = () => { // router diff --git a/web/layouts/user-profile-layout/layout.tsx b/web/layouts/user-profile-layout/layout.tsx index 52bfc6fbf..243eaed1a 100644 --- a/web/layouts/user-profile-layout/layout.tsx +++ b/web/layouts/user-profile-layout/layout.tsx @@ -1,6 +1,7 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { ProfileNavbar, ProfileSidebar } from "components/profile"; import { useUser } from "hooks/store"; // components import { ProfileNavbar, ProfileSidebar } from "components/profile"; diff --git a/web/lib/app-provider.tsx b/web/lib/app-provider.tsx index a91793613..8fcb61744 100644 --- a/web/lib/app-provider.tsx +++ b/web/lib/app-provider.tsx @@ -1,26 +1,24 @@ import { FC, ReactNode } from "react"; +import { observer } from "mobx-react-lite"; import dynamic from "next/dynamic"; import Router from "next/router"; -import NProgress from "nprogress"; -import { observer } from "mobx-react-lite"; import { useTheme } from "next-themes"; -// hooks -import { useApplication, useUser, useWorkspace } from "hooks/store"; +import NProgress from "nprogress"; +import { SWRConfig } from "swr"; // ui import { Toast } from "@plane/ui"; // constants import { SWR_CONFIG } from "constants/swr-config"; -// layouts -import InstanceLayout from "layouts/instance-layout"; -// contexts -import { SWRConfig } from "swr"; //helpers import { resolveGeneralTheme } from "helpers/theme.helper"; +// hooks +import { useApplication, useUser, useWorkspace } from "hooks/store"; +// layouts +import InstanceLayout from "layouts/instance-layout"; // dynamic imports const StoreWrapper = dynamic(() => import("lib/wrappers/store-wrapper"), { ssr: false }); const PostHogProvider = dynamic(() => import("lib/posthog-provider"), { ssr: false }); const CrispWrapper = dynamic(() => import("lib/wrappers/crisp-wrapper"), { ssr: false }); - // nprogress NProgress.configure({ showSpinner: false }); Router.events.on("routeChangeStart", NProgress.start); diff --git a/web/lib/local-storage.ts b/web/lib/local-storage.ts index e0d77dc51..ab84b358f 100644 --- a/web/lib/local-storage.ts +++ b/web/lib/local-storage.ts @@ -3,15 +3,15 @@ import isEmpty from "lodash/isEmpty"; export const storage = { set: (key: string, value: object | string | boolean): void => { if (typeof window === undefined || typeof window === "undefined" || !key || !value) return undefined; - const _value: string | undefined = value + const tempValue: string | undefined = value ? ["string", "boolean"].includes(typeof value) ? value.toString() : isEmpty(value) - ? undefined - : JSON.stringify(value) + ? undefined + : JSON.stringify(value) : undefined; - if (!_value) return undefined; - window.localStorage.setItem(key, _value); + if (!tempValue) return undefined; + window.localStorage.setItem(key, tempValue); }, get: (key: string): string | undefined => { diff --git a/web/lib/posthog-provider.tsx b/web/lib/posthog-provider.tsx index c5acd2957..80391ba95 100644 --- a/web/lib/posthog-provider.tsx +++ b/web/lib/posthog-provider.tsx @@ -2,12 +2,12 @@ import { FC, ReactNode, useEffect, useState } from "react"; import { useRouter } from "next/router"; import posthog from "posthog-js"; import { PostHogProvider as PHProvider } from "posthog-js/react"; -// mobx store provider -import { IUser } from "@plane/types"; -// helpers -import { getUserRole } from "helpers/user.helper"; // constants import { GROUP_WORKSPACE } from "constants/event-tracker"; +// helpers +import { getUserRole } from "helpers/user.helper"; +// types +import { IUser } from "@plane/types"; export interface IPosthogWrapper { children: ReactNode; @@ -59,7 +59,7 @@ const PostHogProvider: FC = (props) => { posthog?.identify(user.email); posthog?.group(GROUP_WORKSPACE, currentWorkspaceId); } - }, [currentWorkspaceId, user]); + }, [currentWorkspaceId, lastWorkspaceId, user]); useEffect(() => { // Track page views diff --git a/web/lib/types.d.ts b/web/lib/types.d.ts index 2b03f6975..8dac1ff82 100644 --- a/web/lib/types.d.ts +++ b/web/lib/types.d.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/ban-types export type NextPageWithLayout

= NextPage & { getLayout?: (page: ReactElement) => ReactNode; }; diff --git a/web/lib/wrappers/crisp-wrapper.tsx b/web/lib/wrappers/crisp-wrapper.tsx index beacf916b..d2771abd8 100644 --- a/web/lib/wrappers/crisp-wrapper.tsx +++ b/web/lib/wrappers/crisp-wrapper.tsx @@ -4,8 +4,8 @@ import { IUser } from "@plane/types"; declare global { interface Window { - $crisp: any; - CRISP_WEBSITE_ID: any; + $crisp: unknown[]; + CRISP_WEBSITE_ID: unknown; } } @@ -22,8 +22,8 @@ const CrispWrapper: FC = (props) => { window.$crisp = []; window.CRISP_WEBSITE_ID = process.env.NEXT_PUBLIC_CRISP_ID; (function () { - var d = document; - var s = d.createElement("script"); + const d = document; + const s = d.createElement("script"); s.src = "https://client.crisp.chat/l.js"; s.async = true; d.getElementsByTagName("head")[0].appendChild(s); diff --git a/web/lib/wrappers/store-wrapper.tsx b/web/lib/wrappers/store-wrapper.tsx index 83867f557..1890bba50 100644 --- a/web/lib/wrappers/store-wrapper.tsx +++ b/web/lib/wrappers/store-wrapper.tsx @@ -1,12 +1,12 @@ import { ReactNode, useEffect, useState, FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import useSWR from "swr"; +import { useRouter } from "next/router"; import { useTheme } from "next-themes"; -// hooks -import { useApplication, useUser } from "hooks/store"; +import useSWR from "swr"; // helpers import { applyTheme, unsetCustomCssVariables } from "helpers/theme.helper"; +// hooks +import { useApplication, useUser } from "hooks/store"; interface IStoreWrapper { children: ReactNode; @@ -15,7 +15,7 @@ interface IStoreWrapper { const StoreWrapper: FC = observer((props) => { const { children } = props; // states - const [dom, setDom] = useState(); + const [dom, setDom] = useState(); // router const router = useRouter(); // store hooks diff --git a/web/package.json b/web/package.json index fbec571ef..99e351191 100644 --- a/web/package.json +++ b/web/package.json @@ -70,11 +70,7 @@ "@types/react-color": "^3.0.6", "@types/react-dom": "^18.2.17", "@types/uuid": "^8.3.4", - "@typescript-eslint/eslint-plugin": "^5.48.2", - "@typescript-eslint/parser": "^5.48.2", - "eslint": "^8.31.0", "eslint-config-custom": "*", - "eslint-config-next": "12.2.2", "prettier": "^2.8.7", "tailwind-config-custom": "*", "tsconfig": "*", diff --git a/web/pages/404.tsx b/web/pages/404.tsx index a73cd2074..639a77333 100644 --- a/web/pages/404.tsx +++ b/web/pages/404.tsx @@ -1,17 +1,17 @@ import React from "react"; -import Link from "next/link"; +import type { NextPage } from "next"; import Image from "next/image"; +import Link from "next/link"; // components +import { Button } from "@plane/ui"; import { PageHead } from "components/core"; // layouts import DefaultLayout from "layouts/default-layout"; // ui -import { Button } from "@plane/ui"; // images import Image404 from "public/404.svg"; // types -import type { NextPage } from "next"; const PageNotFound: NextPage = () => ( diff --git a/web/pages/[workspaceSlug]/active-cycles.tsx b/web/pages/[workspaceSlug]/active-cycles.tsx index f366ddbd6..b7e3b4100 100644 --- a/web/pages/[workspaceSlug]/active-cycles.tsx +++ b/web/pages/[workspaceSlug]/active-cycles.tsx @@ -5,11 +5,11 @@ import { PageHead } from "components/core"; import { WorkspaceActiveCycleHeader } from "components/headers"; import { WorkspaceActiveCyclesUpgrade } from "components/workspace"; // layouts +import { useWorkspace } from "hooks/store"; import { AppLayout } from "layouts/app-layout"; // types import { NextPageWithLayout } from "lib/types"; // hooks -import { useWorkspace } from "hooks/store"; const WorkspaceActiveCyclesPage: NextPageWithLayout = observer(() => { const { currentWorkspace } = useWorkspace(); diff --git a/web/pages/[workspaceSlug]/analytics.tsx b/web/pages/[workspaceSlug]/analytics.tsx index 31c396b54..658f3e34c 100644 --- a/web/pages/[workspaceSlug]/analytics.tsx +++ b/web/pages/[workspaceSlug]/analytics.tsx @@ -1,21 +1,21 @@ import React, { Fragment, ReactElement } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { Tab } from "@headlessui/react"; +import { useRouter } from "next/router"; import { useTheme } from "next-themes"; +import { Tab } from "@headlessui/react"; // hooks -import { useApplication, useEventTracker, useProject, useUser, useWorkspace } from "hooks/store"; // layouts -import { AppLayout } from "layouts/app-layout"; // components -import { PageHead } from "components/core"; import { CustomAnalytics, ScopeAndDemand } from "components/analytics"; -import { WorkspaceAnalyticsHeader } from "components/headers"; +import { PageHead } from "components/core"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { WorkspaceAnalyticsHeader } from "components/headers"; // constants import { ANALYTICS_TABS } from "constants/analytics"; -import { EUserWorkspaceRoles } from "constants/workspace"; import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EUserWorkspaceRoles } from "constants/workspace"; +import { useApplication, useEventTracker, useProject, useUser, useWorkspace } from "hooks/store"; +import { AppLayout } from "layouts/app-layout"; // type import { NextPageWithLayout } from "lib/types"; diff --git a/web/pages/[workspaceSlug]/index.tsx b/web/pages/[workspaceSlug]/index.tsx index 8a6782de8..0011e2619 100644 --- a/web/pages/[workspaceSlug]/index.tsx +++ b/web/pages/[workspaceSlug]/index.tsx @@ -1,15 +1,15 @@ import { ReactElement } from "react"; import { observer } from "mobx-react"; // layouts -import { AppLayout } from "layouts/app-layout"; // components import { PageHead } from "components/core"; -import { WorkspaceDashboardView } from "components/page-views"; import { WorkspaceDashboardHeader } from "components/headers/workspace-dashboard"; +import { WorkspaceDashboardView } from "components/page-views"; // types -import { NextPageWithLayout } from "lib/types"; // hooks import { useWorkspace } from "hooks/store"; +import { AppLayout } from "layouts/app-layout"; +import { NextPageWithLayout } from "lib/types"; const WorkspacePage: NextPageWithLayout = observer(() => { const { currentWorkspace } = useWorkspace(); diff --git a/web/pages/[workspaceSlug]/profile/[userId]/activity.tsx b/web/pages/[workspaceSlug]/profile/[userId]/activity.tsx index 09269676a..87029724e 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/activity.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/activity.tsx @@ -1,20 +1,20 @@ import { ReactElement, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react"; +import { useRouter } from "next/router"; // hooks +import { Button } from "@plane/ui"; +import { UserProfileHeader } from "components/headers"; +import { DownloadActivityButton, WorkspaceActivityListPage } from "components/profile"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { useUser } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { ProfileAuthWrapper } from "layouts/user-profile-layout"; // components -import { UserProfileHeader } from "components/headers"; -import { DownloadActivityButton, WorkspaceActivityListPage } from "components/profile"; // ui -import { Button } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; const PER_PAGE = 100; diff --git a/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx b/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx index 1cef81e78..9d1dbf072 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx @@ -1,13 +1,13 @@ import React, { ReactElement } from "react"; // layouts +import { PageHead } from "components/core"; +import { UserProfileHeader } from "components/headers"; +import { ProfileIssuesPage } from "components/profile/profile-issues"; import { AppLayout } from "layouts/app-layout"; import { ProfileAuthWrapper } from "layouts/user-profile-layout"; // components -import { UserProfileHeader } from "components/headers"; -import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; -import { ProfileIssuesPage } from "components/profile/profile-issues"; const ProfileAssignedIssuesPage: NextPageWithLayout = () => ( <> diff --git a/web/pages/[workspaceSlug]/profile/[userId]/created.tsx b/web/pages/[workspaceSlug]/profile/[userId]/created.tsx index 47a8445d7..105d9d309 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/created.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/created.tsx @@ -2,14 +2,14 @@ import { ReactElement } from "react"; // store import { observer } from "mobx-react-lite"; // layouts +import { PageHead } from "components/core"; +import { UserProfileHeader } from "components/headers"; +import { ProfileIssuesPage } from "components/profile/profile-issues"; import { AppLayout } from "layouts/app-layout"; import { ProfileAuthWrapper } from "layouts/user-profile-layout"; // components -import { UserProfileHeader } from "components/headers"; -import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; -import { ProfileIssuesPage } from "components/profile/profile-issues"; const ProfileCreatedIssuesPage: NextPageWithLayout = () => ( <> diff --git a/web/pages/[workspaceSlug]/profile/[userId]/index.tsx b/web/pages/[workspaceSlug]/profile/[userId]/index.tsx index 6e8a10b50..eb71989ed 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/index.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/index.tsx @@ -2,13 +2,10 @@ import { ReactElement } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; // services -import { UserService } from "services/user.service"; // layouts -import { AppLayout } from "layouts/app-layout"; -import { ProfileAuthWrapper } from "layouts/user-profile-layout"; // components -import { UserProfileHeader } from "components/headers"; import { PageHead } from "components/core"; +import { UserProfileHeader } from "components/headers"; import { ProfileActivity, ProfilePriorityDistribution, @@ -17,11 +14,14 @@ import { ProfileWorkload, } from "components/profile"; // types -import { IUserStateDistribution, TStateGroups } from "@plane/types"; -import { NextPageWithLayout } from "lib/types"; // constants import { USER_PROFILE_DATA } from "constants/fetch-keys"; import { GROUP_CHOICES } from "constants/project"; +import { AppLayout } from "layouts/app-layout"; +import { ProfileAuthWrapper } from "layouts/user-profile-layout"; +import { NextPageWithLayout } from "lib/types"; +import { UserService } from "services/user.service"; +import { IUserStateDistribution, TStateGroups } from "@plane/types"; // services const userService = new UserService(); diff --git a/web/pages/[workspaceSlug]/profile/[userId]/subscribed.tsx b/web/pages/[workspaceSlug]/profile/[userId]/subscribed.tsx index c05c39302..c81ed6918 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/subscribed.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/subscribed.tsx @@ -2,14 +2,14 @@ import { ReactElement } from "react"; // store import { observer } from "mobx-react-lite"; // layouts +import { PageHead } from "components/core"; +import { UserProfileHeader } from "components/headers"; +import { ProfileIssuesPage } from "components/profile/profile-issues"; import { AppLayout } from "layouts/app-layout"; import { ProfileAuthWrapper } from "layouts/user-profile-layout"; // components -import { UserProfileHeader } from "components/headers"; -import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; -import { ProfileIssuesPage } from "components/profile/profile-issues"; const ProfileSubscribedIssuesPage: NextPageWithLayout = () => ( <> diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/index.tsx index 34019c026..353f0a8b6 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/index.tsx @@ -1,17 +1,17 @@ import { ReactElement } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react"; +import { useRouter } from "next/router"; // layouts +import { PageHead } from "components/core"; +import { ProjectArchivedIssuesHeader } from "components/headers"; +import { ArchivedIssueLayoutRoot } from "components/issues"; +import { useProject } from "hooks/store"; import { AppLayout } from "layouts/app-layout"; // contexts -import { ArchivedIssueLayoutRoot } from "components/issues"; // components -import { ProjectArchivedIssuesHeader } from "components/headers"; -import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; // hooks -import { useProject } from "hooks/store"; const ProjectArchivedIssuesPage: NextPageWithLayout = observer(() => { // router diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx index 7b5ec8833..6eaef6c0f 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx @@ -1,23 +1,23 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react"; // hooks +import { EmptyState } from "components/common"; +import { PageHead } from "components/core"; +import { CycleDetailsSidebar } from "components/cycles"; +import { CycleIssuesHeader } from "components/headers"; +import { CycleLayoutRoot } from "components/issues/issue-layouts"; import { useCycle, useProject } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // layouts import { AppLayout } from "layouts/app-layout"; // components -import { PageHead } from "components/core"; -import { CycleIssuesHeader } from "components/headers"; -import { CycleDetailsSidebar } from "components/cycles"; -import { CycleLayoutRoot } from "components/issues/issue-layouts"; // ui -import { EmptyState } from "components/common"; // assets +import { NextPageWithLayout } from "lib/types"; import emptyCycle from "public/empty-state/cycle.svg"; // types -import { NextPageWithLayout } from "lib/types"; const CycleDetailPage: NextPageWithLayout = observer(() => { // router diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index 0f86089aa..ac2b760ef 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -1,28 +1,28 @@ import { Fragment, useCallback, useState, ReactElement } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { Tab } from "@headlessui/react"; +import { useRouter } from "next/router"; import { useTheme } from "next-themes"; +import { Tab } from "@headlessui/react"; // hooks +import { Tooltip } from "@plane/ui"; +import { PageHead } from "components/core"; +import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { CyclesHeader } from "components/headers"; +import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui"; +import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle"; +import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { useEventTracker, useCycle, useUser, useProject } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // layouts import { AppLayout } from "layouts/app-layout"; // components -import { PageHead } from "components/core"; -import { CyclesHeader } from "components/headers"; -import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui -import { Tooltip } from "@plane/ui"; -import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui"; // types -import { TCycleView, TCycleLayout } from "@plane/types"; import { NextPageWithLayout } from "lib/types"; +import { TCycleView, TCycleLayout } from "@plane/types"; // constants -import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state"; const ProjectCyclesPage: NextPageWithLayout = observer(() => { const [createModal, setCreateModal] = useState(false); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/draft-issues/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/draft-issues/index.tsx index bf11063c3..c506e55b0 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/draft-issues/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/draft-issues/index.tsx @@ -1,17 +1,17 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import { X, PenSquare } from "lucide-react"; // layouts -import { AppLayout } from "layouts/app-layout"; // components -import { DraftIssueLayoutRoot } from "components/issues/issue-layouts/roots/draft-issue-layout-root"; import { PageHead } from "components/core"; import { ProjectDraftIssueHeader } from "components/headers"; +import { DraftIssueLayoutRoot } from "components/issues/issue-layouts/roots/draft-issue-layout-root"; // types -import { NextPageWithLayout } from "lib/types"; // hooks import { useProject } from "hooks/store"; -import { observer } from "mobx-react"; +import { AppLayout } from "layouts/app-layout"; +import { NextPageWithLayout } from "lib/types"; const ProjectDraftIssuesPage: NextPageWithLayout = observer(() => { const router = useRouter(); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx index de412c9d7..f8fb1aa47 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx @@ -1,16 +1,16 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react"; // hooks +import { PageHead } from "components/core"; +import { ProjectInboxHeader } from "components/headers"; +import { InboxSidebarRoot, InboxContentRoot } from "components/inbox"; +import { InboxLayoutLoader } from "components/ui"; import { useProject, useInboxIssues } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; // components -import { InboxLayoutLoader } from "components/ui"; -import { PageHead } from "components/core"; -import { ProjectInboxHeader } from "components/headers"; -import { InboxSidebarRoot, InboxContentRoot } from "components/inbox"; // types import { NextPageWithLayout } from "lib/types"; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/inbox/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/inbox/index.tsx index 1021ad102..c3d3f2e5a 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/inbox/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/inbox/index.tsx @@ -1,15 +1,15 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react"; // hooks +import { ProjectInboxHeader } from "components/headers"; +import { InboxLayoutLoader } from "components/ui"; import { useInbox, useProject } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; // ui -import { InboxLayoutLoader } from "components/ui"; // components -import { ProjectInboxHeader } from "components/headers"; // types import { NextPageWithLayout } from "lib/types"; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx index 6ff7d5aa5..54994ab6d 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx @@ -1,19 +1,19 @@ import React, { ReactElement, useEffect } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react-lite"; // layouts -import { AppLayout } from "layouts/app-layout"; -// components +import { Loader } from "@plane/ui"; import { PageHead } from "components/core"; +// components import { ProjectIssueDetailsHeader } from "components/headers"; import { IssueDetailRoot } from "components/issues"; // ui -import { Loader } from "@plane/ui"; // types -import { NextPageWithLayout } from "lib/types"; // store hooks import { useApplication, useIssueDetail, useProject } from "hooks/store"; +import { AppLayout } from "layouts/app-layout"; +import { NextPageWithLayout } from "lib/types"; const IssueDetailsPage: NextPageWithLayout = observer(() => { // router diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx index 2aa9ab2e6..241af79c4 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx @@ -1,17 +1,17 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import Head from "next/head"; import { useRouter } from "next/router"; -import { observer } from "mobx-react"; // components -import { ProjectLayoutRoot } from "components/issues"; +import { PageHead } from "components/core"; import { ProjectIssuesHeader } from "components/headers"; +import { ProjectLayoutRoot } from "components/issues"; // types +import { useProject } from "hooks/store"; +import { AppLayout } from "layouts/app-layout"; import { NextPageWithLayout } from "lib/types"; // layouts -import { AppLayout } from "layouts/app-layout"; // hooks -import { useProject } from "hooks/store"; -import { PageHead } from "components/core"; const ProjectIssuesPage: NextPageWithLayout = observer(() => { const router = useRouter(); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx index afbd97b8e..e55eea170 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx @@ -1,22 +1,22 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react"; // hooks +import { EmptyState } from "components/common"; +import { PageHead } from "components/core"; +import { ModuleIssuesHeader } from "components/headers"; +import { ModuleLayoutRoot } from "components/issues"; +import { ModuleDetailsSidebar } from "components/modules"; import { useModule, useProject } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // layouts import { AppLayout } from "layouts/app-layout"; // components -import { ModuleDetailsSidebar } from "components/modules"; -import { ModuleLayoutRoot } from "components/issues"; -import { ModuleIssuesHeader } from "components/headers"; -import { PageHead } from "components/core"; -import { EmptyState } from "components/common"; // assets +import { NextPageWithLayout } from "lib/types"; import emptyModule from "public/empty-state/module.svg"; // types -import { NextPageWithLayout } from "lib/types"; const ModuleIssuesPage: NextPageWithLayout = observer(() => { // router diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx index 085f1e3c3..3648f5922 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx @@ -1,16 +1,16 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; // layouts -import { AppLayout } from "layouts/app-layout"; // components import { PageHead } from "components/core"; -import { ModulesListView } from "components/modules"; import { ModulesListHeader } from "components/headers"; +import { ModulesListView } from "components/modules"; // types -import { NextPageWithLayout } from "lib/types"; // hooks import { useProject } from "hooks/store"; -import { observer } from "mobx-react"; +import { AppLayout } from "layouts/app-layout"; +import { NextPageWithLayout } from "lib/types"; const ProjectModulesPage: NextPageWithLayout = observer(() => { const router = useRouter(); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx index c44f6186e..3a133ee50 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx @@ -1,33 +1,33 @@ -import { Sparkle } from "lucide-react"; -import { observer } from "mobx-react-lite"; -import useSWR from "swr"; -import { useRouter } from "next/router"; import { ReactElement, useEffect, useRef, useState } from "react"; +import { DocumentEditorWithRef, DocumentReadOnlyEditorWithRef } from "@plane/document-editor"; +import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; +import useSWR from "swr"; +import { Sparkle } from "lucide-react"; // hooks -import { useApplication, usePage, useUser, useWorkspace } from "hooks/store"; -import useReloadConfirmations from "hooks/use-reload-confirmation"; -// services -import { FileService } from "services/file.service"; -// layouts -import { AppLayout } from "layouts/app-layout"; -// components +import { Spinner, TOAST_TYPE, setToast } from "@plane/ui"; import { GptAssistantPopover, PageHead } from "components/core"; import { PageDetailsHeader } from "components/headers/page-details"; +import { IssuePeekOverview } from "components/issues"; +import { EUserProjectRoles } from "constants/project"; +import { useApplication, usePage, useUser, useWorkspace } from "hooks/store"; +import { useProjectPages } from "hooks/store/use-project-specific-pages"; +import useReloadConfirmations from "hooks/use-reload-confirmation"; +// services +import { AppLayout } from "layouts/app-layout"; +import { NextPageWithLayout } from "lib/types"; +import { FileService } from "services/file.service"; +// layouts +// components // ui -import { DocumentEditorWithRef, DocumentReadOnlyEditorWithRef } from "@plane/document-editor"; -import { Spinner, TOAST_TYPE, setToast } from "@plane/ui"; // assets // helpers // types import { IPage } from "@plane/types"; -import { NextPageWithLayout } from "lib/types"; // fetch-keys // constants -import { EUserProjectRoles } from "constants/project"; -import { useProjectPages } from "hooks/store/use-project-specific-pages"; -import { IssuePeekOverview } from "components/issues"; // services const fileService = new FileService(); @@ -311,7 +311,7 @@ const PageDetailsPage: NextPageWithLayout = observer(() => { updatePageTitle={updatePageTitle} onActionCompleteHandler={actionCompleteAlert} customClassName="tracking-tight self-center h-full w-full right-[0.675rem]" - onChange={(_description_json: Object, description_html: string) => { + onChange={(_description_json: any, description_html: string) => { setShowAlert(true); onChange(description_html); handleSubmit(updatePage)(); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx index a8c85ef8d..45204541b 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx @@ -1,30 +1,30 @@ import { useState, Fragment, ReactElement } from "react"; -import { useRouter } from "next/router"; -import dynamic from "next/dynamic"; -import { Tab } from "@headlessui/react"; -import useSWR from "swr"; import { observer } from "mobx-react-lite"; +import dynamic from "next/dynamic"; +import { useRouter } from "next/router"; import { useTheme } from "next-themes"; +import useSWR from "swr"; +import { Tab } from "@headlessui/react"; // hooks +import { PageHead } from "components/core"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { PagesHeader } from "components/headers"; +import { RecentPagesList, CreateUpdatePageModal } from "components/pages"; +import { PagesLoader } from "components/ui"; +import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { PAGE_TABS_LIST } from "constants/page"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { useApplication, useEventTracker, useUser, useProject } from "hooks/store"; +import { useProjectPages } from "hooks/store/use-project-page"; import useLocalStorage from "hooks/use-local-storage"; import useUserAuth from "hooks/use-user-auth"; import useSize from "hooks/use-window-size"; // layouts import { AppLayout } from "layouts/app-layout"; // components -import { RecentPagesList, CreateUpdatePageModal } from "components/pages"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { PagesHeader } from "components/headers"; -import { PagesLoader } from "components/ui"; // types import { NextPageWithLayout } from "lib/types"; // constants -import { PAGE_TABS_LIST } from "constants/page"; -import { useProjectPages } from "hooks/store/use-project-page"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { PageHead } from "components/core"; const AllPagesList = dynamic(() => import("components/pages").then((a) => a.AllPagesList), { ssr: false, diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx index 1cefb9418..d6724c789 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx @@ -1,22 +1,25 @@ import React, { ReactElement } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks -import { useProject, useUser } from "hooks/store"; -// layouts -import { AppLayout } from "layouts/app-layout"; -import { ProjectSettingLayout } from "layouts/settings-layout"; -// ui import { TOAST_TYPE, setToast } from "@plane/ui"; -// components import { AutoArchiveAutomation, AutoCloseAutomation } from "components/automation"; +// layouts +// ui +// components import { PageHead } from "components/core"; import { ProjectSettingHeader } from "components/headers"; +import { EUserProjectRoles } from "constants/project"; +import { useProject, useUser } from "hooks/store"; +import { AppLayout } from "layouts/app-layout"; +// layouts +import { ProjectSettingLayout } from "layouts/settings-layout"; +// hooks +// components // types import { NextPageWithLayout } from "lib/types"; import { IProject } from "@plane/types"; // constants -import { EUserProjectRoles } from "constants/project"; const AutomationSettingsPage: NextPageWithLayout = observer(() => { // router diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx index 70108f90a..c1aea645f 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx @@ -1,18 +1,18 @@ import { ReactElement } from "react"; import { observer } from "mobx-react-lite"; // hooks +import { PageHead } from "components/core"; +import { EstimatesList } from "components/estimates"; +import { ProjectSettingHeader } from "components/headers"; +import { EUserProjectRoles } from "constants/project"; import { useUser, useProject } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; // components -import { PageHead } from "components/core"; -import { ProjectSettingHeader } from "components/headers"; -import { EstimatesList } from "components/estimates"; // types import { NextPageWithLayout } from "lib/types"; // constants -import { EUserProjectRoles } from "constants/project"; const EstimatesSettingsPage: NextPageWithLayout = observer(() => { const { diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx index b618437ab..e36ebd9a8 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx @@ -1,16 +1,16 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react"; // hooks +import { PageHead } from "components/core"; +import { ProjectSettingHeader } from "components/headers"; +import { ProjectFeaturesList } from "components/project"; import { useProject, useUser } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; // components -import { PageHead } from "components/core"; -import { ProjectSettingHeader } from "components/headers"; -import { ProjectFeaturesList } from "components/project"; // types import { NextPageWithLayout } from "lib/types"; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx index 347d64f84..037e47434 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx @@ -1,13 +1,8 @@ import { useState, ReactElement } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react-lite"; // hooks -import { useProject } from "hooks/store"; -// layouts -import { AppLayout } from "layouts/app-layout"; -import { ProjectSettingLayout } from "layouts/settings-layout"; -// components import { PageHead } from "components/core"; import { ProjectSettingHeader } from "components/headers"; import { @@ -16,6 +11,11 @@ import { ProjectDetailsForm, ProjectDetailsFormLoader, } from "components/project"; +import { useProject } from "hooks/store"; +// layouts +import { AppLayout } from "layouts/app-layout"; +import { ProjectSettingLayout } from "layouts/settings-layout"; +// components // types import { NextPageWithLayout } from "lib/types"; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx index 5c9faae7c..b227becf9 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx @@ -1,29 +1,29 @@ import { ReactElement } from "react"; -import { useRouter } from "next/router"; -import useSWR from "swr"; -import { useTheme } from "next-themes"; import { observer } from "mobx-react"; +import { useRouter } from "next/router"; +import { useTheme } from "next-themes"; +import useSWR from "swr"; // hooks +import { PageHead } from "components/core"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { ProjectSettingHeader } from "components/headers"; +import { IntegrationCard } from "components/project"; +import { IntegrationsSettingsLoader } from "components/ui"; +import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys"; import { useUser } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; // services +import { NextPageWithLayout } from "lib/types"; import { IntegrationService } from "services/integrations"; import { ProjectService } from "services/project"; // components -import { PageHead } from "components/core"; -import { IntegrationCard } from "components/project"; -import { ProjectSettingHeader } from "components/headers"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui -import { IntegrationsSettingsLoader } from "components/ui"; // types import { IProject } from "@plane/types"; -import { NextPageWithLayout } from "lib/types"; // fetch-keys -import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys"; -import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; // services const integrationService = new IntegrationService(); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx index d62ac1e66..8b3758829 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx @@ -1,16 +1,16 @@ import { ReactElement } from "react"; import { observer } from "mobx-react"; // layouts +import { PageHead } from "components/core"; +import { ProjectSettingHeader } from "components/headers"; +import { ProjectSettingsLabelList } from "components/labels"; +import { useProject } from "hooks/store"; import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; // components -import { PageHead } from "components/core"; -import { ProjectSettingsLabelList } from "components/labels"; -import { ProjectSettingHeader } from "components/headers"; // types import { NextPageWithLayout } from "lib/types"; // hooks -import { useProject } from "hooks/store"; const LabelsSettingsPage: NextPageWithLayout = observer(() => { const { currentProjectDetails } = useProject(); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx index f74d464d5..551dde0c2 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx @@ -1,16 +1,16 @@ import { ReactElement } from "react"; import { observer } from "mobx-react"; // layouts -import { AppLayout } from "layouts/app-layout"; -import { ProjectSettingLayout } from "layouts/settings-layout"; // components import { PageHead } from "components/core"; import { ProjectSettingHeader } from "components/headers"; import { ProjectMemberList, ProjectSettingsMemberDefaults } from "components/project"; // types -import { NextPageWithLayout } from "lib/types"; // hooks import { useProject } from "hooks/store"; +import { AppLayout } from "layouts/app-layout"; +import { ProjectSettingLayout } from "layouts/settings-layout"; +import { NextPageWithLayout } from "lib/types"; const MembersSettingsPage: NextPageWithLayout = observer(() => { // store diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx index 57451e699..4a5c290d8 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx @@ -1,16 +1,16 @@ import { ReactElement } from "react"; import { observer } from "mobx-react"; // layout +import { PageHead } from "components/core"; +import { ProjectSettingHeader } from "components/headers"; +import { ProjectSettingStateList } from "components/states"; +import { useProject } from "hooks/store"; import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; // components -import { ProjectSettingStateList } from "components/states"; -import { ProjectSettingHeader } from "components/headers"; -import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; // hook -import { useProject } from "hooks/store"; const StatesSettingsPage: NextPageWithLayout = observer(() => { // store diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx index 2ac6b2e00..17ba29394 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx @@ -1,21 +1,21 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react"; // hooks +import { EmptyState } from "components/common"; +import { PageHead } from "components/core"; +import { ProjectViewIssuesHeader } from "components/headers"; +import { ProjectViewLayoutRoot } from "components/issues"; import { useProject, useProjectView } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; // components -import { ProjectViewLayoutRoot } from "components/issues"; -import { ProjectViewIssuesHeader } from "components/headers"; -import { PageHead } from "components/core"; // ui -import { EmptyState } from "components/common"; // assets +import { NextPageWithLayout } from "lib/types"; import emptyView from "public/empty-state/view.svg"; // types -import { NextPageWithLayout } from "lib/types"; const ProjectViewIssuesPage: NextPageWithLayout = observer(() => { // router diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx index 33be5d102..9864ef391 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx @@ -1,10 +1,10 @@ import { ReactElement } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react"; +import { useRouter } from "next/router"; // components +import { PageHead } from "components/core"; import { ProjectViewsHeader } from "components/headers"; import { ProjectViewsList } from "components/views"; -import { PageHead } from "components/core"; // hooks import { useProject } from "hooks/store"; // layouts diff --git a/web/pages/[workspaceSlug]/projects/index.tsx b/web/pages/[workspaceSlug]/projects/index.tsx index 1a145a2d1..158e6577f 100644 --- a/web/pages/[workspaceSlug]/projects/index.tsx +++ b/web/pages/[workspaceSlug]/projects/index.tsx @@ -2,13 +2,13 @@ import { ReactElement } from "react"; import { observer } from "mobx-react"; // components import { PageHead } from "components/core"; -import { ProjectCardList } from "components/project"; import { ProjectsHeader } from "components/headers"; +import { ProjectCardList } from "components/project"; // layouts +import { useWorkspace } from "hooks/store"; import { AppLayout } from "layouts/app-layout"; // type import { NextPageWithLayout } from "lib/types"; -import { useWorkspace } from "hooks/store"; const ProjectsPage: NextPageWithLayout = observer(() => { // store diff --git a/web/pages/[workspaceSlug]/settings/api-tokens.tsx b/web/pages/[workspaceSlug]/settings/api-tokens.tsx index 35366cb0a..75d46b63d 100644 --- a/web/pages/[workspaceSlug]/settings/api-tokens.tsx +++ b/web/pages/[workspaceSlug]/settings/api-tokens.tsx @@ -1,29 +1,29 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import useSWR from "swr"; +import { useRouter } from "next/router"; import { useTheme } from "next-themes"; +import useSWR from "swr"; // store hooks +import { Button } from "@plane/ui"; +import { ApiTokenListItem, CreateApiTokenModal } from "components/api-token"; +import { PageHead } from "components/core"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { WorkspaceSettingHeader } from "components/headers"; +import { APITokenSettingsLoader } from "components/ui"; +import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { API_TOKENS_LIST } from "constants/fetch-keys"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { useUser, useWorkspace } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // component -import { WorkspaceSettingHeader } from "components/headers"; -import { ApiTokenListItem, CreateApiTokenModal } from "components/api-token"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui -import { Button } from "@plane/ui"; -import { APITokenSettingsLoader } from "components/ui"; // services +import { NextPageWithLayout } from "lib/types"; import { APITokenService } from "services/api_token.service"; // types -import { NextPageWithLayout } from "lib/types"; // constants -import { API_TOKENS_LIST } from "constants/fetch-keys"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { PageHead } from "components/core"; const apiTokenService = new APITokenService(); diff --git a/web/pages/[workspaceSlug]/settings/billing.tsx b/web/pages/[workspaceSlug]/settings/billing.tsx index f4f5d5397..bd1114f85 100644 --- a/web/pages/[workspaceSlug]/settings/billing.tsx +++ b/web/pages/[workspaceSlug]/settings/billing.tsx @@ -1,18 +1,18 @@ import { observer } from "mobx-react-lite"; // hooks +import { Button } from "@plane/ui"; +import { PageHead } from "components/core"; +import { WorkspaceSettingHeader } from "components/headers"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { useUser, useWorkspace } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // component -import { WorkspaceSettingHeader } from "components/headers"; -import { PageHead } from "components/core"; // ui -import { Button } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; const BillingSettingsPage: NextPageWithLayout = observer(() => { // store hooks diff --git a/web/pages/[workspaceSlug]/settings/exports.tsx b/web/pages/[workspaceSlug]/settings/exports.tsx index c124a6423..a6f958472 100644 --- a/web/pages/[workspaceSlug]/settings/exports.tsx +++ b/web/pages/[workspaceSlug]/settings/exports.tsx @@ -1,17 +1,17 @@ import { observer } from "mobx-react-lite"; // hooks +import { PageHead } from "components/core"; +import ExportGuide from "components/exporter/guide"; +import { WorkspaceSettingHeader } from "components/headers"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { useUser, useWorkspace } from "hooks/store"; // layout import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // components -import { WorkspaceSettingHeader } from "components/headers"; -import ExportGuide from "components/exporter/guide"; -import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; const ExportsPage: NextPageWithLayout = observer(() => { // store hooks diff --git a/web/pages/[workspaceSlug]/settings/imports.tsx b/web/pages/[workspaceSlug]/settings/imports.tsx index 5178209d2..19eeeac66 100644 --- a/web/pages/[workspaceSlug]/settings/imports.tsx +++ b/web/pages/[workspaceSlug]/settings/imports.tsx @@ -1,17 +1,17 @@ import { observer } from "mobx-react-lite"; // hooks +import { PageHead } from "components/core"; +import { WorkspaceSettingHeader } from "components/headers"; +import IntegrationGuide from "components/integration/guide"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { useUser, useWorkspace } from "hooks/store"; // layouts -import { WorkspaceSettingLayout } from "layouts/settings-layout"; import { AppLayout } from "layouts/app-layout"; +import { WorkspaceSettingLayout } from "layouts/settings-layout"; // components -import IntegrationGuide from "components/integration/guide"; -import { WorkspaceSettingHeader } from "components/headers"; -import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; const ImportsPage: NextPageWithLayout = observer(() => { // store hooks diff --git a/web/pages/[workspaceSlug]/settings/index.tsx b/web/pages/[workspaceSlug]/settings/index.tsx index 2924b13c4..37ce39335 100644 --- a/web/pages/[workspaceSlug]/settings/index.tsx +++ b/web/pages/[workspaceSlug]/settings/index.tsx @@ -1,14 +1,14 @@ import { ReactElement } from "react"; import { observer } from "mobx-react"; // layouts +import { PageHead } from "components/core"; +import { WorkspaceSettingHeader } from "components/headers"; +import { WorkspaceDetails } from "components/workspace"; +import { useWorkspace } from "hooks/store"; import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // hooks -import { useWorkspace } from "hooks/store"; // components -import { WorkspaceSettingHeader } from "components/headers"; -import { WorkspaceDetails } from "components/workspace"; -import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; diff --git a/web/pages/[workspaceSlug]/settings/integrations.tsx b/web/pages/[workspaceSlug]/settings/integrations.tsx index 500533877..0aa54f60a 100644 --- a/web/pages/[workspaceSlug]/settings/integrations.tsx +++ b/web/pages/[workspaceSlug]/settings/integrations.tsx @@ -1,26 +1,26 @@ import { ReactElement } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; // hooks -import { useUser, useWorkspace } from "hooks/store"; // services -import { IntegrationService } from "services/integrations"; // layouts -import { AppLayout } from "layouts/app-layout"; -import { WorkspaceSettingLayout } from "layouts/settings-layout"; // components -import { SingleIntegrationCard } from "components/integration"; -import { WorkspaceSettingHeader } from "components/headers"; import { PageHead } from "components/core"; +import { WorkspaceSettingHeader } from "components/headers"; +import { SingleIntegrationCard } from "components/integration"; // ui import { IntegrationAndImportExportBanner, IntegrationsSettingsLoader } from "components/ui"; // types -import { NextPageWithLayout } from "lib/types"; // fetch-keys import { APP_INTEGRATIONS } from "constants/fetch-keys"; // constants import { EUserWorkspaceRoles } from "constants/workspace"; +import { useUser, useWorkspace } from "hooks/store"; +import { AppLayout } from "layouts/app-layout"; +import { WorkspaceSettingLayout } from "layouts/settings-layout"; +import { NextPageWithLayout } from "lib/types"; +import { IntegrationService } from "services/integrations"; const integrationService = new IntegrationService(); diff --git a/web/pages/[workspaceSlug]/settings/members.tsx b/web/pages/[workspaceSlug]/settings/members.tsx index f635588c2..e1be1d889 100644 --- a/web/pages/[workspaceSlug]/settings/members.tsx +++ b/web/pages/[workspaceSlug]/settings/members.tsx @@ -1,26 +1,26 @@ import { useState, ReactElement } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Search } from "lucide-react"; // hooks +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; +import { PageHead } from "components/core"; +import { WorkspaceSettingHeader } from "components/headers"; +import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "components/workspace"; +import { MEMBER_INVITED } from "constants/event-tracker"; +import { EUserWorkspaceRoles } from "constants/workspace"; +import { getUserRole } from "helpers/user.helper"; import { useEventTracker, useMember, useUser, useWorkspace } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // components -import { WorkspaceSettingHeader } from "components/headers"; -import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "components/workspace"; -import { PageHead } from "components/core"; // ui -import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; import { IWorkspaceBulkInviteFormData } from "@plane/types"; // helpers -import { getUserRole } from "helpers/user.helper"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; -import { MEMBER_INVITED } from "constants/event-tracker"; const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => { // states @@ -30,7 +30,7 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => { const router = useRouter(); const { workspaceSlug } = router.query; // store hooks - const { captureEvent, setTrackElement } = useEventTracker(); + const { captureEvent } = useEventTracker(); const { membership: { currentWorkspaceRole }, } = useUser(); diff --git a/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx b/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx index bafaa3aaa..263f90963 100644 --- a/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx +++ b/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx @@ -1,18 +1,19 @@ import { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; // hooks +import { Spinner, TOAST_TYPE, setToast } from "@plane/ui"; + +import { PageHead } from "components/core"; +import { WorkspaceSettingHeader } from "components/headers"; +import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "components/web-hooks"; import { useUser, useWebhook, useWorkspace } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // components -import { WorkspaceSettingHeader } from "components/headers"; -import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "components/web-hooks"; -import { PageHead } from "components/core"; // ui -import { Spinner, TOAST_TYPE, setToast } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; import { IWebhook } from "@plane/types"; diff --git a/web/pages/[workspaceSlug]/settings/webhooks/index.tsx b/web/pages/[workspaceSlug]/settings/webhooks/index.tsx index 19f23913e..d5058e29f 100644 --- a/web/pages/[workspaceSlug]/settings/webhooks/index.tsx +++ b/web/pages/[workspaceSlug]/settings/webhooks/index.tsx @@ -1,25 +1,25 @@ import React, { useEffect, useState } from "react"; -import { useRouter } from "next/router"; -import useSWR from "swr"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { useTheme } from "next-themes"; +import useSWR from "swr"; // hooks +import { Button } from "@plane/ui"; +import { PageHead } from "components/core"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { WorkspaceSettingHeader } from "components/headers"; +import { WebhookSettingsLoader } from "components/ui"; +import { WebhooksList, CreateWebhookModal } from "components/web-hooks"; +import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; import { useUser, useWebhook, useWorkspace } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // components -import { WorkspaceSettingHeader } from "components/headers"; -import { WebhooksList, CreateWebhookModal } from "components/web-hooks"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui -import { Button } from "@plane/ui"; -import { WebhookSettingsLoader } from "components/ui"; // types import { NextPageWithLayout } from "lib/types"; // constants -import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { PageHead } from "components/core"; const WebhooksListPage: NextPageWithLayout = observer(() => { // states diff --git a/web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx b/web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx index 85e907481..7d736e8f9 100644 --- a/web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx +++ b/web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx @@ -1,19 +1,19 @@ import { ReactElement } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react"; +import { useRouter } from "next/router"; // layouts +import { PageHead } from "components/core"; +import { GlobalIssuesHeader } from "components/headers"; +import { AllIssueLayoutRoot } from "components/issues"; +import { GlobalViewsHeader } from "components/workspace"; +import { DEFAULT_GLOBAL_VIEWS_LIST } from "constants/workspace"; +import { useGlobalView, useWorkspace } from "hooks/store"; import { AppLayout } from "layouts/app-layout"; // hooks -import { useGlobalView, useWorkspace } from "hooks/store"; // components -import { GlobalViewsHeader } from "components/workspace"; -import { AllIssueLayoutRoot } from "components/issues"; -import { GlobalIssuesHeader } from "components/headers"; -import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; // constants -import { DEFAULT_GLOBAL_VIEWS_LIST } from "constants/workspace"; const GlobalViewIssuesPage: NextPageWithLayout = observer(() => { // router @@ -29,8 +29,8 @@ const GlobalViewIssuesPage: NextPageWithLayout = observer(() => { currentWorkspace?.name && defaultView?.label ? `${currentWorkspace?.name} - ${defaultView?.label}` : currentWorkspace?.name && globalViewDetails?.name - ? `${currentWorkspace?.name} - ${globalViewDetails?.name}` - : undefined; + ? `${currentWorkspace?.name} - ${globalViewDetails?.name}` + : undefined; return ( <> diff --git a/web/pages/[workspaceSlug]/workspace-views/index.tsx b/web/pages/[workspaceSlug]/workspace-views/index.tsx index 61fdcf058..ccd7ac485 100644 --- a/web/pages/[workspaceSlug]/workspace-views/index.tsx +++ b/web/pages/[workspaceSlug]/workspace-views/index.tsx @@ -1,21 +1,21 @@ import React, { useState, ReactElement } from "react"; import { observer } from "mobx-react"; // layouts -import { AppLayout } from "layouts/app-layout"; // components -import { PageHead } from "components/core"; -import { GlobalDefaultViewListItem, GlobalViewsList } from "components/workspace"; -import { GlobalIssuesHeader } from "components/headers"; // ui +import { Search } from "lucide-react"; import { Input } from "@plane/ui"; // icons -import { Search } from "lucide-react"; +import { PageHead } from "components/core"; +import { GlobalIssuesHeader } from "components/headers"; +import { GlobalDefaultViewListItem, GlobalViewsList } from "components/workspace"; // types -import { NextPageWithLayout } from "lib/types"; // constants import { DEFAULT_GLOBAL_VIEWS_LIST } from "constants/workspace"; // hooks import { useWorkspace } from "hooks/store"; +import { AppLayout } from "layouts/app-layout"; +import { NextPageWithLayout } from "lib/types"; const WorkspaceViewsPage: NextPageWithLayout = observer(() => { const [query, setQuery] = useState(""); diff --git a/web/pages/_document.tsx b/web/pages/_document.tsx index cc0411068..ccaa34a40 100644 --- a/web/pages/_document.tsx +++ b/web/pages/_document.tsx @@ -1,5 +1,6 @@ import Document, { Html, Head, Main, NextScript } from "next/document"; // constants +import Script from "next/script"; import { SITE_NAME, SITE_DESCRIPTION, @@ -8,7 +9,6 @@ import { SITE_KEYWORDS, SITE_TITLE, } from "constants/seo-variables"; -import Script from "next/script"; class MyDocument extends Document { render() { diff --git a/web/pages/_error.tsx b/web/pages/_error.tsx index 0a530cf9f..81e0daecd 100644 --- a/web/pages/_error.tsx +++ b/web/pages/_error.tsx @@ -3,11 +3,12 @@ import * as Sentry from "@sentry/nextjs"; import { useRouter } from "next/router"; // services +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; + +import DefaultLayout from "layouts/default-layout"; import { AuthService } from "services/auth.service"; // layouts -import DefaultLayout from "layouts/default-layout"; // ui -import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // services const authService = new AuthService(); diff --git a/web/pages/accounts/sign-up.tsx b/web/pages/accounts/sign-up.tsx index cba9c0166..c40a1660b 100644 --- a/web/pages/accounts/sign-up.tsx +++ b/web/pages/accounts/sign-up.tsx @@ -1,19 +1,19 @@ import React from "react"; -import Image from "next/image"; import { observer } from "mobx-react-lite"; +import Image from "next/image"; // hooks +import { Spinner } from "@plane/ui"; +import { SignUpRoot } from "components/account"; +import { PageHead } from "components/core"; import { useApplication, useUser } from "hooks/store"; // layouts import DefaultLayout from "layouts/default-layout"; // components -import { SignUpRoot } from "components/account"; -import { PageHead } from "components/core"; // ui -import { Spinner } from "@plane/ui"; // assets +import { NextPageWithLayout } from "lib/types"; import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; // types -import { NextPageWithLayout } from "lib/types"; const SignUpPage: NextPageWithLayout = observer(() => { // store hooks diff --git a/web/pages/create-workspace.tsx b/web/pages/create-workspace.tsx index 952ed0b68..629e4a379 100644 --- a/web/pages/create-workspace.tsx +++ b/web/pages/create-workspace.tsx @@ -1,23 +1,23 @@ import React, { useState, ReactElement } from "react"; -import { useRouter } from "next/router"; -import Image from "next/image"; -import { useTheme } from "next-themes"; import { observer } from "mobx-react-lite"; +import Image from "next/image"; import Link from "next/link"; +import { useRouter } from "next/router"; +import { useTheme } from "next-themes"; // hooks +import { PageHead } from "components/core"; +import { CreateWorkspaceForm } from "components/workspace"; import { useUser } from "hooks/store"; // layouts -import DefaultLayout from "layouts/default-layout"; import { UserAuthWrapper } from "layouts/auth-layout"; +import DefaultLayout from "layouts/default-layout"; // components -import { CreateWorkspaceForm } from "components/workspace"; -import { PageHead } from "components/core"; // images +import { NextPageWithLayout } from "lib/types"; import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg"; import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg"; // types import { IWorkspace } from "@plane/types"; -import { NextPageWithLayout } from "lib/types"; const CreateWorkspacePage: NextPageWithLayout = observer(() => { // router diff --git a/web/pages/god-mode/ai.tsx b/web/pages/god-mode/ai.tsx index b84e98098..35b652d9b 100644 --- a/web/pages/god-mode/ai.tsx +++ b/web/pages/god-mode/ai.tsx @@ -1,19 +1,19 @@ import { ReactElement } from "react"; -import useSWR from "swr"; import { observer } from "mobx-react-lite"; +import useSWR from "swr"; // layouts +import { Lightbulb } from "lucide-react"; +import { Loader } from "@plane/ui"; +import { PageHead } from "components/core"; +import { InstanceAIForm } from "components/instance"; +import { useApplication } from "hooks/store"; import { InstanceAdminLayout } from "layouts/admin-layout"; // types import { NextPageWithLayout } from "lib/types"; // hooks -import { useApplication } from "hooks/store"; // ui -import { Loader } from "@plane/ui"; // icons -import { Lightbulb } from "lucide-react"; // components -import { InstanceAIForm } from "components/instance"; -import { PageHead } from "components/core"; const InstanceAdminAIPage: NextPageWithLayout = observer(() => { // store diff --git a/web/pages/god-mode/authorization.tsx b/web/pages/god-mode/authorization.tsx index 6274fca20..f4eeefc65 100644 --- a/web/pages/god-mode/authorization.tsx +++ b/web/pages/god-mode/authorization.tsx @@ -1,18 +1,19 @@ import { ReactElement, useState } from "react"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import useSWR from "swr"; -import { observer } from "mobx-react-lite"; // layouts +import { Loader, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; + +import { PageHead } from "components/core"; +import { InstanceGithubConfigForm, InstanceGoogleConfigForm } from "components/instance"; +import { useApplication } from "hooks/store"; import { InstanceAdminLayout } from "layouts/admin-layout"; // types import { NextPageWithLayout } from "lib/types"; // hooks -import { useApplication } from "hooks/store"; // ui -import { Loader, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; // components -import { InstanceGithubConfigForm, InstanceGoogleConfigForm } from "components/instance"; -import { PageHead } from "components/core"; const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => { // store diff --git a/web/pages/god-mode/email.tsx b/web/pages/god-mode/email.tsx index 65889607f..0e4a594ce 100644 --- a/web/pages/god-mode/email.tsx +++ b/web/pages/god-mode/email.tsx @@ -1,17 +1,17 @@ import { ReactElement } from "react"; -import useSWR from "swr"; import { observer } from "mobx-react-lite"; +import useSWR from "swr"; // layouts +import { Loader } from "@plane/ui"; +import { PageHead } from "components/core"; +import { InstanceEmailForm } from "components/instance"; +import { useApplication } from "hooks/store"; import { InstanceAdminLayout } from "layouts/admin-layout"; // types import { NextPageWithLayout } from "lib/types"; // hooks -import { useApplication } from "hooks/store"; // ui -import { Loader } from "@plane/ui"; // components -import { InstanceEmailForm } from "components/instance"; -import { PageHead } from "components/core"; const InstanceAdminEmailPage: NextPageWithLayout = observer(() => { // store diff --git a/web/pages/god-mode/image.tsx b/web/pages/god-mode/image.tsx index 349dccf4b..4c6abaa96 100644 --- a/web/pages/god-mode/image.tsx +++ b/web/pages/god-mode/image.tsx @@ -1,17 +1,17 @@ import { ReactElement } from "react"; -import useSWR from "swr"; import { observer } from "mobx-react-lite"; +import useSWR from "swr"; // layouts +import { Loader } from "@plane/ui"; +import { PageHead } from "components/core"; +import { InstanceImageConfigForm } from "components/instance"; +import { useApplication } from "hooks/store"; import { InstanceAdminLayout } from "layouts/admin-layout"; // types import { NextPageWithLayout } from "lib/types"; // hooks -import { useApplication } from "hooks/store"; // ui -import { Loader } from "@plane/ui"; // components -import { InstanceImageConfigForm } from "components/instance"; -import { PageHead } from "components/core"; const InstanceAdminImagePage: NextPageWithLayout = observer(() => { // store diff --git a/web/pages/god-mode/index.tsx b/web/pages/god-mode/index.tsx index a93abad31..a7cb29c05 100644 --- a/web/pages/god-mode/index.tsx +++ b/web/pages/god-mode/index.tsx @@ -1,17 +1,17 @@ import { ReactElement } from "react"; -import useSWR from "swr"; import { observer } from "mobx-react-lite"; +import useSWR from "swr"; // layouts +import { Loader } from "@plane/ui"; +import { PageHead } from "components/core"; +import { InstanceGeneralForm } from "components/instance"; +import { useApplication } from "hooks/store"; import { InstanceAdminLayout } from "layouts/admin-layout"; // types import { NextPageWithLayout } from "lib/types"; // hooks -import { useApplication } from "hooks/store"; // ui -import { Loader } from "@plane/ui"; // components -import { InstanceGeneralForm } from "components/instance"; -import { PageHead } from "components/core"; const InstanceAdminPage: NextPageWithLayout = observer(() => { // store hooks diff --git a/web/pages/index.tsx b/web/pages/index.tsx index 2f8b32394..d9e99811f 100644 --- a/web/pages/index.tsx +++ b/web/pages/index.tsx @@ -1,8 +1,8 @@ import { ReactElement } from "react"; // layouts +import { SignInView } from "components/page-views"; import DefaultLayout from "layouts/default-layout"; // components -import { SignInView } from "components/page-views"; // type import { NextPageWithLayout } from "lib/types"; diff --git a/web/pages/installations/[provider]/index.tsx b/web/pages/installations/[provider]/index.tsx index 85bf21539..052782dc5 100644 --- a/web/pages/installations/[provider]/index.tsx +++ b/web/pages/installations/[provider]/index.tsx @@ -1,11 +1,11 @@ import React, { useEffect, ReactElement } from "react"; import { useRouter } from "next/router"; // services -import { AppInstallationService } from "services/app_installation.service"; // ui import { Spinner } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; +import { AppInstallationService } from "services/app_installation.service"; // services const appInstallationService = new AppInstallationService(); diff --git a/web/pages/invitations/index.tsx b/web/pages/invitations/index.tsx index 18441f0a0..7f976865b 100644 --- a/web/pages/invitations/index.tsx +++ b/web/pages/invitations/index.tsx @@ -77,7 +77,7 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { workspaceService .joinWorkspaces({ invitations: invitationsRespond }) - .then((res) => { + .then(() => { mutate("USER_WORKSPACES"); const firstInviteId = invitationsRespond[0]; const invitation = invitations?.find((i) => i.id === firstInviteId); @@ -85,6 +85,7 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { joinWorkspaceMetricGroup(redirectWorkspace?.id); captureEvent(MEMBER_ACCEPTED, { member_id: invitation?.id, + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain role: getUserRole(invitation?.role!), project_id: undefined, accepted_from: "App", diff --git a/web/pages/onboarding/index.tsx b/web/pages/onboarding/index.tsx index 5b5b91280..2ebd61f3a 100644 --- a/web/pages/onboarding/index.tsx +++ b/web/pages/onboarding/index.tsx @@ -1,32 +1,32 @@ import { useEffect, useState, ReactElement } from "react"; -import Image from "next/image"; -import { useTheme } from "next-themes"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import useSWR from "swr"; -import { ChevronDown } from "lucide-react"; -import { Menu, Transition } from "@headlessui/react"; +import Image from "next/image"; +import { useRouter } from "next/router"; +import { useTheme } from "next-themes"; import { Controller, useForm } from "react-hook-form"; +import useSWR from "swr"; +import { Menu, Transition } from "@headlessui/react"; +import { ChevronDown } from "lucide-react"; // hooks +import { Avatar, Spinner } from "@plane/ui"; +import { PageHead } from "components/core"; +import { InviteMembers, JoinWorkspaces, UserDetails, SwitchOrDeleteAccountModal } from "components/onboarding"; +import { USER_ONBOARDING_COMPLETED } from "constants/event-tracker"; import { useEventTracker, useUser, useWorkspace } from "hooks/store"; import useUserAuth from "hooks/use-user-auth"; // services +import { UserAuthWrapper } from "layouts/auth-layout"; +import DefaultLayout from "layouts/default-layout"; +import { NextPageWithLayout } from "lib/types"; +import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; import { WorkspaceService } from "services/workspace.service"; // layouts -import DefaultLayout from "layouts/default-layout"; -import { UserAuthWrapper } from "layouts/auth-layout"; // components -import { InviteMembers, JoinWorkspaces, UserDetails, SwitchOrDeleteAccountModal } from "components/onboarding"; -import { PageHead } from "components/core"; // ui -import { Avatar, Spinner } from "@plane/ui"; // images -import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; // types import { IUser, TOnboardingSteps } from "@plane/types"; -import { NextPageWithLayout } from "lib/types"; // constants -import { USER_ONBOARDING_COMPLETED } from "constants/event-tracker"; // services const workspaceService = new WorkspaceService(); @@ -166,8 +166,8 @@ const OnboardingPage: NextPageWithLayout = observer(() => { currentUser?.first_name ? `${currentUser?.first_name} ${currentUser?.last_name ?? ""}` : value.length > 0 - ? value - : currentUser?.email + ? value + : currentUser?.email } src={currentUser?.avatar} size={35} @@ -182,8 +182,8 @@ const OnboardingPage: NextPageWithLayout = observer(() => { {currentUser?.first_name ? `${currentUser?.first_name} ${currentUser?.last_name ?? ""}` : value.length > 0 - ? value - : null} + ? value + : null}

)} diff --git a/web/pages/profile/activity.tsx b/web/pages/profile/activity.tsx index b0e8bb1a0..bda1295cf 100644 --- a/web/pages/profile/activity.tsx +++ b/web/pages/profile/activity.tsx @@ -1,15 +1,15 @@ import { ReactElement, useState } from "react"; import { observer } from "mobx-react"; //hooks +import { Button } from "@plane/ui"; +import { PageHead } from "components/core"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { ProfileActivityListPage } from "components/profile"; import { useApplication } from "hooks/store"; // layouts import { ProfileSettingsLayout } from "layouts/settings-layout"; // components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { ProfileActivityListPage } from "components/profile"; -import { PageHead } from "components/core"; // ui -import { Button } from "@plane/ui"; // type import { NextPageWithLayout } from "lib/types"; diff --git a/web/pages/profile/change-password.tsx b/web/pages/profile/change-password.tsx index f37a2b6a6..7e9344753 100644 --- a/web/pages/profile/change-password.tsx +++ b/web/pages/profile/change-password.tsx @@ -1,20 +1,20 @@ import { ReactElement, useEffect, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; // hooks +import { Button, Input, Spinner, TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; +import { PageHead } from "components/core"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { useApplication, useUser } from "hooks/store"; // services -import { UserService } from "services/user.service"; // components -import { PageHead } from "components/core"; // layout import { ProfileSettingsLayout } from "layouts/settings-layout"; // ui -import { Button, Input, Spinner, TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { UserService } from "services/user.service"; interface FormValues { old_password: string; @@ -85,8 +85,8 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => { return ( <> -
-
+
+
themeStore.toggleSidebar()} />
= { avatar: "", @@ -143,8 +148,8 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { return ( <> -
-
+
+
themeStore.toggleSidebar()} />
@@ -167,7 +172,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { )} /> setDeactivateAccountModal(false)} /> -
+
@@ -303,7 +308,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { ref={ref} hasError={Boolean(errors.email)} placeholder="Enter your email" - className={`w-full rounded-md cursor-not-allowed !bg-custom-background-80 ${ + className={`w-full cursor-not-allowed rounded-md !bg-custom-background-80 ${ errors.email ? "border-red-500" : "" }`} disabled diff --git a/web/pages/profile/preferences/email.tsx b/web/pages/profile/preferences/email.tsx index ddd23abdf..b34a493e5 100644 --- a/web/pages/profile/preferences/email.tsx +++ b/web/pages/profile/preferences/email.tsx @@ -1,16 +1,16 @@ import { ReactElement } from "react"; import useSWR from "swr"; // layouts +import { PageHead } from "components/core"; +import { EmailNotificationForm } from "components/profile/preferences"; +import { EmailSettingsLoader } from "components/ui"; import { ProfilePreferenceSettingsLayout } from "layouts/settings-layout/profile/preferences"; // ui -import { EmailSettingsLoader } from "components/ui"; // components -import { EmailNotificationForm } from "components/profile/preferences"; -import { PageHead } from "components/core"; // services +import { NextPageWithLayout } from "lib/types"; import { UserService } from "services/user.service"; // type -import { NextPageWithLayout } from "lib/types"; // services const userService = new UserService(); diff --git a/web/pages/profile/preferences/theme.tsx b/web/pages/profile/preferences/theme.tsx index 94540aeda..e23e94c66 100644 --- a/web/pages/profile/preferences/theme.tsx +++ b/web/pages/profile/preferences/theme.tsx @@ -1,16 +1,16 @@ import { useEffect, useState, ReactElement } from "react"; import { observer } from "mobx-react-lite"; import { useTheme } from "next-themes"; +// ui +import { Spinner, setPromiseToast } from "@plane/ui"; +// components +import { CustomThemeSelector, ThemeSwitch, PageHead } from "components/core"; +// constants +import { I_THEME_OPTION, THEME_OPTIONS } from "constants/themes"; // hooks import { useUser } from "hooks/store"; // layouts import { ProfilePreferenceSettingsLayout } from "layouts/settings-layout/profile/preferences"; -// components -import { CustomThemeSelector, ThemeSwitch, PageHead } from "components/core"; -// ui -import { Spinner, setPromiseToast } from "@plane/ui"; -// constants -import { I_THEME_OPTION, THEME_OPTIONS } from "constants/themes"; // type import { NextPageWithLayout } from "lib/types"; @@ -54,7 +54,7 @@ const ProfilePreferencesThemePage: NextPageWithLayout = observer(() => { <> {currentUser ? ( -
+

Preferences

diff --git a/web/pages/workspace-invitations/index.tsx b/web/pages/workspace-invitations/index.tsx index aa95d0a38..74c881125 100644 --- a/web/pages/workspace-invitations/index.tsx +++ b/web/pages/workspace-invitations/index.tsx @@ -1,22 +1,22 @@ import React, { ReactElement } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import useSWR from "swr"; import { Boxes, Check, Share2, Star, User2, X } from "lucide-react"; -import { observer } from "mobx-react-lite"; // hooks +import { Spinner } from "@plane/ui"; +import { EmptySpace, EmptySpaceItem } from "components/ui/empty-space"; +import { WORKSPACE_INVITATION } from "constants/fetch-keys"; import { useUser } from "hooks/store"; // services -import { WorkspaceService } from "services/workspace.service"; // layouts import DefaultLayout from "layouts/default-layout"; // ui -import { Spinner } from "@plane/ui"; // icons -import { EmptySpace, EmptySpaceItem } from "components/ui/empty-space"; // types import { NextPageWithLayout } from "lib/types"; +import { WorkspaceService } from "services/workspace.service"; // constants -import { WORKSPACE_INVITATION } from "constants/fetch-keys"; // services const workspaceService = new WorkspaceService(); diff --git a/web/services/ai.service.ts b/web/services/ai.service.ts index 11c489c1f..677f50e92 100644 --- a/web/services/ai.service.ts +++ b/web/services/ai.service.ts @@ -1,8 +1,8 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import { IGptResponse } from "@plane/types"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; export class AIService extends APIService { constructor() { diff --git a/web/services/analytics.service.ts b/web/services/analytics.service.ts index 5e3aac44b..972fe36ea 100644 --- a/web/services/analytics.service.ts +++ b/web/services/analytics.service.ts @@ -1,4 +1,5 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import { @@ -9,7 +10,6 @@ import { ISaveAnalyticsFormData, } from "@plane/types"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; export class AnalyticsService extends APIService { constructor() { diff --git a/web/services/api_token.service.ts b/web/services/api_token.service.ts index 76a24798f..3979f6e1f 100644 --- a/web/services/api_token.service.ts +++ b/web/services/api_token.service.ts @@ -1,6 +1,6 @@ import { API_BASE_URL } from "helpers/common.helper"; -import { APIService } from "./api.service"; import { IApiToken } from "@plane/types"; +import { APIService } from "./api.service"; export class APITokenService extends APIService { constructor() { diff --git a/web/services/app_config.service.ts b/web/services/app_config.service.ts index 4b45e0cc4..7c2d1e24e 100644 --- a/web/services/app_config.service.ts +++ b/web/services/app_config.service.ts @@ -1,7 +1,7 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helper -import { API_BASE_URL } from "helpers/common.helper"; // types import { IAppConfig } from "@plane/types"; diff --git a/web/services/app_installation.service.ts b/web/services/app_installation.service.ts index 179721036..055a4b091 100644 --- a/web/services/app_installation.service.ts +++ b/web/services/app_installation.service.ts @@ -1,7 +1,7 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; export class AppInstallationService extends APIService { constructor() { diff --git a/web/services/auth.service.ts b/web/services/auth.service.ts index f47a52824..f90fafc66 100644 --- a/web/services/auth.service.ts +++ b/web/services/auth.service.ts @@ -1,7 +1,7 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import { IEmailCheckData, diff --git a/web/services/cycle.service.ts b/web/services/cycle.service.ts index 5e13e3b8e..f7ee8a0ab 100644 --- a/web/services/cycle.service.ts +++ b/web/services/cycle.service.ts @@ -1,9 +1,9 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import type { CycleDateCheckData, ICycle, TIssue } from "@plane/types"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; export class CycleService extends APIService { constructor() { diff --git a/web/services/dashboard.service.ts b/web/services/dashboard.service.ts index e001f92a1..b1138899d 100644 --- a/web/services/dashboard.service.ts +++ b/web/services/dashboard.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import { THomeDashboardResponse, TWidget, TWidgetStatsResponse, TWidgetStatsRequestParams } from "@plane/types"; diff --git a/web/services/file.service.ts b/web/services/file.service.ts index d5e80dd53..0818bc992 100644 --- a/web/services/file.service.ts +++ b/web/services/file.service.ts @@ -1,8 +1,8 @@ // services +import axios from "axios"; +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; -import axios from "axios"; export interface UnSplashImage { id: string; diff --git a/web/services/inbox.service.ts b/web/services/inbox.service.ts index a36d356ce..45f0172fb 100644 --- a/web/services/inbox.service.ts +++ b/web/services/inbox.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import type { IInboxIssue, IInbox, TInboxStatus, IInboxQueryParams } from "@plane/types"; diff --git a/web/services/inbox/inbox-issue.service.ts b/web/services/inbox/inbox-issue.service.ts index 6b2099059..e6d52768c 100644 --- a/web/services/inbox/inbox-issue.service.ts +++ b/web/services/inbox/inbox-issue.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import type { TInboxIssueFilterOptions, TInboxIssueExtendedDetail, TIssue, TInboxDetailedStatus } from "@plane/types"; diff --git a/web/services/inbox/inbox.service.ts b/web/services/inbox/inbox.service.ts index 8ee6ee514..fc5fa5a99 100644 --- a/web/services/inbox/inbox.service.ts +++ b/web/services/inbox/inbox.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import type { TInbox } from "@plane/types"; diff --git a/web/services/instance.service.ts b/web/services/instance.service.ts index 1bc5ecdbc..f61370a91 100644 --- a/web/services/instance.service.ts +++ b/web/services/instance.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import type { IFormattedInstanceConfiguration, IInstance, IInstanceAdmin, IInstanceConfiguration } from "@plane/types"; diff --git a/web/services/integrations/github.service.ts b/web/services/integrations/github.service.ts index 6a0519565..5c4c95c09 100644 --- a/web/services/integrations/github.service.ts +++ b/web/services/integrations/github.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import { IGithubRepoInfo, IGithubServiceImportFormData } from "@plane/types"; diff --git a/web/services/integrations/integration.service.ts b/web/services/integrations/integration.service.ts index 460dc17d9..a1bb10078 100644 --- a/web/services/integrations/integration.service.ts +++ b/web/services/integrations/integration.service.ts @@ -1,8 +1,8 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import { IAppIntegration, IImporterService, IWorkspaceIntegration, IExportServiceResponse } from "@plane/types"; // helper -import { API_BASE_URL } from "helpers/common.helper"; export class IntegrationService extends APIService { constructor() { diff --git a/web/services/integrations/jira.service.ts b/web/services/integrations/jira.service.ts index 5641bb28b..8c254bbab 100644 --- a/web/services/integrations/jira.service.ts +++ b/web/services/integrations/jira.service.ts @@ -1,5 +1,5 @@ -import { APIService } from "services/api.service"; import { API_BASE_URL } from "helpers/common.helper"; +import { APIService } from "services/api.service"; // types import { IJiraMetadata, IJiraResponse, IJiraImporterForm } from "@plane/types"; diff --git a/web/services/issue/issue.service.ts b/web/services/issue/issue.service.ts index 316288278..d7f92f792 100644 --- a/web/services/issue/issue.service.ts +++ b/web/services/issue/issue.service.ts @@ -1,9 +1,9 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // type import type { TIssue, IIssueDisplayProperties, TIssueLink, TIssueSubIssues, TIssueActivity } from "@plane/types"; // helper -import { API_BASE_URL } from "helpers/common.helper"; export class IssueService extends APIService { constructor() { diff --git a/web/services/issue/issue_activity.service.ts b/web/services/issue/issue_activity.service.ts index 87c7a8f54..9028568ad 100644 --- a/web/services/issue/issue_activity.service.ts +++ b/web/services/issue/issue_activity.service.ts @@ -1,8 +1,8 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import { TIssueActivity } from "@plane/types"; // helper -import { API_BASE_URL } from "helpers/common.helper"; export class IssueActivityService extends APIService { constructor() { diff --git a/web/services/issue/issue_archive.service.ts b/web/services/issue/issue_archive.service.ts index e2a5132a5..e232e796f 100644 --- a/web/services/issue/issue_archive.service.ts +++ b/web/services/issue/issue_archive.service.ts @@ -1,8 +1,8 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import { TIssue } from "@plane/types"; // constants -import { API_BASE_URL } from "helpers/common.helper"; export class IssueArchiveService extends APIService { constructor() { diff --git a/web/services/issue/issue_attachment.service.ts b/web/services/issue/issue_attachment.service.ts index 16253218a..00673c963 100644 --- a/web/services/issue/issue_attachment.service.ts +++ b/web/services/issue/issue_attachment.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helper -import { API_BASE_URL } from "helpers/common.helper"; // types import { TIssueAttachment } from "@plane/types"; diff --git a/web/services/issue/issue_comment.service.ts b/web/services/issue/issue_comment.service.ts index 8001d644a..d7ef35df7 100644 --- a/web/services/issue/issue_comment.service.ts +++ b/web/services/issue/issue_comment.service.ts @@ -1,8 +1,8 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import { TIssueComment } from "@plane/types"; // helper -import { API_BASE_URL } from "helpers/common.helper"; export class IssueCommentService extends APIService { constructor() { diff --git a/web/services/issue/issue_draft.service.ts b/web/services/issue/issue_draft.service.ts index a93bda776..3ccd43f56 100644 --- a/web/services/issue/issue_draft.service.ts +++ b/web/services/issue/issue_draft.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; import { TIssue } from "@plane/types"; export class IssueDraftService extends APIService { diff --git a/web/services/issue_filter.service.ts b/web/services/issue_filter.service.ts index 5103a4bc8..664666a3b 100644 --- a/web/services/issue_filter.service.ts +++ b/web/services/issue_filter.service.ts @@ -1,8 +1,8 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import type { IIssueFiltersResponse } from "@plane/types"; -import { API_BASE_URL } from "helpers/common.helper"; export class IssueFiltersService extends APIService { constructor() { diff --git a/web/services/module.service.ts b/web/services/module.service.ts index 1efad8a23..9942f691c 100644 --- a/web/services/module.service.ts +++ b/web/services/module.service.ts @@ -1,8 +1,8 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import type { IModule, TIssue, ILinkDetails, ModuleLink } from "@plane/types"; -import { API_BASE_URL } from "helpers/common.helper"; export class ModuleService extends APIService { constructor() { diff --git a/web/services/notification.service.ts b/web/services/notification.service.ts index db9c6d6d1..d12656c09 100644 --- a/web/services/notification.service.ts +++ b/web/services/notification.service.ts @@ -1,4 +1,5 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import type { @@ -9,7 +10,6 @@ import type { IMarkAllAsReadPayload, } from "@plane/types"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; export class NotificationService extends APIService { constructor() { diff --git a/web/services/project/project-estimate.service.ts b/web/services/project/project-estimate.service.ts index 6d276c7b9..880c4dd8d 100644 --- a/web/services/project/project-estimate.service.ts +++ b/web/services/project/project-estimate.service.ts @@ -1,9 +1,9 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import type { IEstimate, IEstimateFormData, IEstimatePoint } from "@plane/types"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; export class ProjectEstimateService extends APIService { constructor() { diff --git a/web/services/project/project-export.service.ts b/web/services/project/project-export.service.ts index b5503a829..cc8cebe71 100644 --- a/web/services/project/project-export.service.ts +++ b/web/services/project/project-export.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; export class ProjectExportService extends APIService { constructor() { diff --git a/web/services/project/project-state.service.ts b/web/services/project/project-state.service.ts index 9f846987e..4087ada30 100644 --- a/web/services/project/project-state.service.ts +++ b/web/services/project/project-state.service.ts @@ -1,7 +1,7 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import type { IState } from "@plane/types"; diff --git a/web/services/user.service.ts b/web/services/user.service.ts index 41111db98..691e6c028 100644 --- a/web/services/user.service.ts +++ b/web/services/user.service.ts @@ -1,4 +1,5 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import type { @@ -12,7 +13,6 @@ import type { IUserEmailNotificationSettings, } from "@plane/types"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; export class UserService extends APIService { constructor() { diff --git a/web/services/view.service.ts b/web/services/view.service.ts index 95ae7dd06..f09eea563 100644 --- a/web/services/view.service.ts +++ b/web/services/view.service.ts @@ -1,8 +1,8 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import { IProjectView } from "@plane/types"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; export class ViewService extends APIService { constructor() { diff --git a/web/services/webhook.service.ts b/web/services/webhook.service.ts index abfe7c46d..d021799fb 100644 --- a/web/services/webhook.service.ts +++ b/web/services/webhook.service.ts @@ -1,7 +1,7 @@ // api services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import { IWebhook } from "@plane/types"; diff --git a/web/services/workspace.service.ts b/web/services/workspace.service.ts index 2515853f5..bfeadad03 100644 --- a/web/services/workspace.service.ts +++ b/web/services/workspace.service.ts @@ -1,7 +1,7 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import { IWorkspace, diff --git a/web/store/application/app-config.store.ts b/web/store/application/app-config.store.ts index 6faef8b69..aec22a4ce 100644 --- a/web/store/application/app-config.store.ts +++ b/web/store/application/app-config.store.ts @@ -1,8 +1,8 @@ import { observable, action, makeObservable, runInAction } from "mobx"; // types +import { AppConfigService } from "services/app_config.service"; import { IAppConfig } from "@plane/types"; // services -import { AppConfigService } from "services/app_config.service"; export interface IAppConfigStore { // observables diff --git a/web/store/application/command-palette.store.ts b/web/store/application/command-palette.store.ts index 22b395e34..dc10bba88 100644 --- a/web/store/application/command-palette.store.ts +++ b/web/store/application/command-palette.store.ts @@ -1,8 +1,8 @@ import { observable, action, makeObservable, computed } from "mobx"; // services -import { ProjectService } from "services/project"; -import { PageService } from "services/page.service"; import { EIssuesStoreType, TCreateModalStoreTypes } from "constants/issue"; +import { PageService } from "services/page.service"; +import { ProjectService } from "services/project"; export interface ModalData { store: EIssuesStoreType; diff --git a/web/store/application/index.ts b/web/store/application/index.ts index 30333535a..bad28d4c9 100644 --- a/web/store/application/index.ts +++ b/web/store/application/index.ts @@ -1,7 +1,7 @@ import { RootStore } from "store/root.store"; +import { EventTrackerStore, IEventTrackerStore } from "../event-tracker.store"; import { AppConfigStore, IAppConfigStore } from "./app-config.store"; import { CommandPaletteStore, ICommandPaletteStore } from "./command-palette.store"; -import { EventTrackerStore, IEventTrackerStore } from "../event-tracker.store"; // import { EventTrackerStore, IEventTrackerStore } from "./event-tracker.store"; import { InstanceStore, IInstanceStore } from "./instance.store"; import { RouterStore, IRouterStore } from "./router.store"; diff --git a/web/store/application/instance.store.ts b/web/store/application/instance.store.ts index 7c486ef8b..b4793fdfb 100644 --- a/web/store/application/instance.store.ts +++ b/web/store/application/instance.store.ts @@ -1,8 +1,8 @@ import { observable, action, computed, makeObservable, runInAction } from "mobx"; // types +import { InstanceService } from "services/instance.service"; import { IInstance, IInstanceConfiguration, IFormattedInstanceConfiguration, IInstanceAdmin } from "@plane/types"; // services -import { InstanceService } from "services/instance.service"; export interface IInstanceStore { // issues diff --git a/web/store/cycle.store.ts b/web/store/cycle.store.ts index ee4842539..aea87033e 100644 --- a/web/store/cycle.store.ts +++ b/web/store/cycle.store.ts @@ -1,16 +1,16 @@ -import { action, computed, observable, makeObservable, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; import { isFuture, isPast, isToday } from "date-fns"; import set from "lodash/set"; import sortBy from "lodash/sortBy"; +import { action, computed, observable, makeObservable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; // types -import { ICycle, CycleDateCheckData } from "@plane/types"; // mobx -import { RootStore } from "store/root.store"; // services -import { ProjectService } from "services/project"; -import { IssueService } from "services/issue"; import { CycleService } from "services/cycle.service"; +import { IssueService } from "services/issue"; +import { ProjectService } from "services/project"; +import { RootStore } from "store/root.store"; +import { ICycle, CycleDateCheckData } from "@plane/types"; export interface ICycleStore { //Loaders diff --git a/web/store/dashboard.store.ts b/web/store/dashboard.store.ts index ad0960c7b..c8a07428e 100644 --- a/web/store/dashboard.store.ts +++ b/web/store/dashboard.store.ts @@ -1,6 +1,6 @@ +import set from "lodash/set"; import { action, computed, makeObservable, observable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; -import set from "lodash/set"; // services import { DashboardService } from "services/dashboard.service"; // types diff --git a/web/store/estimate.store.ts b/web/store/estimate.store.ts index beddd52ab..9c197ffaa 100644 --- a/web/store/estimate.store.ts +++ b/web/store/estimate.store.ts @@ -1,11 +1,11 @@ -import { observable, action, makeObservable, runInAction, computed } from "mobx"; import set from "lodash/set"; +import { observable, action, makeObservable, runInAction, computed } from "mobx"; // services +import { computedFn } from "mobx-utils"; import { ProjectEstimateService } from "services/project"; // types import { RootStore } from "store/root.store"; import { IEstimate, IEstimateFormData } from "@plane/types"; -import { computedFn } from "mobx-utils"; export interface IEstimateStore { //Loaders diff --git a/web/store/event-tracker.store.ts b/web/store/event-tracker.store.ts index 744ad44fb..f117e6cbc 100644 --- a/web/store/event-tracker.store.ts +++ b/web/store/event-tracker.store.ts @@ -1,7 +1,6 @@ import { action, computed, makeObservable, observable } from "mobx"; import posthog from "posthog-js"; // stores -import { RootStore } from "./root.store"; import { GROUP_WORKSPACE, WORKSPACE_CREATED, @@ -15,6 +14,7 @@ import { getWorkspaceEventPayload, getPageEventPayload, } from "constants/event-tracker"; +import { RootStore } from "./root.store"; export interface IEventTrackerStore { // properties diff --git a/web/store/global-view.store.ts b/web/store/global-view.store.ts index 65aedadb5..60d97f633 100644 --- a/web/store/global-view.store.ts +++ b/web/store/global-view.store.ts @@ -1,6 +1,6 @@ +import { set } from "lodash"; import { observable, action, makeObservable, runInAction, computed } from "mobx"; import { computedFn } from "mobx-utils"; -import { set } from "lodash"; // services import { WorkspaceService } from "services/workspace.service"; // types diff --git a/web/store/inbox/inbox.store.ts b/web/store/inbox/inbox.store.ts index 8d8f2bec5..803af3095 100644 --- a/web/store/inbox/inbox.store.ts +++ b/web/store/inbox/inbox.store.ts @@ -1,9 +1,9 @@ +import concat from "lodash/concat"; +import set from "lodash/set"; +import uniq from "lodash/uniq"; +import update from "lodash/update"; import { observable, action, makeObservable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; -import set from "lodash/set"; -import update from "lodash/update"; -import concat from "lodash/concat"; -import uniq from "lodash/uniq"; // services import { InboxService } from "services/inbox/inbox.service"; // types diff --git a/web/store/inbox/inbox_filter.store.ts b/web/store/inbox/inbox_filter.store.ts index c4566acbe..8bad22cdd 100644 --- a/web/store/inbox/inbox_filter.store.ts +++ b/web/store/inbox/inbox_filter.store.ts @@ -1,6 +1,6 @@ -import { observable, action, makeObservable, runInAction, computed } from "mobx"; -import set from "lodash/set"; import isEmpty from "lodash/isEmpty"; +import set from "lodash/set"; +import { observable, action, makeObservable, runInAction, computed } from "mobx"; // services import { InboxService } from "services/inbox.service"; // types diff --git a/web/store/inbox/inbox_issue.store.ts b/web/store/inbox/inbox_issue.store.ts index 4f980357f..2ecbedff0 100644 --- a/web/store/inbox/inbox_issue.store.ts +++ b/web/store/inbox/inbox_issue.store.ts @@ -1,10 +1,10 @@ +import concat from "lodash/concat"; +import pull from "lodash/pull"; +import set from "lodash/set"; +import uniq from "lodash/uniq"; +import update from "lodash/update"; import { observable, action, makeObservable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; -import set from "lodash/set"; -import update from "lodash/update"; -import concat from "lodash/concat"; -import uniq from "lodash/uniq"; -import pull from "lodash/pull"; // services import { InboxIssueService } from "services/inbox/inbox-issue.service"; // types diff --git a/web/store/inbox/root.store.ts b/web/store/inbox/root.store.ts index b0706cca7..982de47bc 100644 --- a/web/store/inbox/root.store.ts +++ b/web/store/inbox/root.store.ts @@ -1,8 +1,8 @@ // types import { RootStore } from "store/root.store"; import { IInbox, Inbox } from "./inbox.store"; -import { IInboxIssue, InboxIssue } from "./inbox_issue.store"; import { IInboxFilter, InboxFilter } from "./inbox_filter.store"; +import { IInboxIssue, InboxIssue } from "./inbox_issue.store"; export interface IInboxRootStore { rootStore: RootStore; diff --git a/web/store/issue/archived/filter.store.ts b/web/store/issue/archived/filter.store.ts index 032928cda..12d541bea 100644 --- a/web/store/issue/archived/filter.store.ts +++ b/web/store/issue/archived/filter.store.ts @@ -1,14 +1,12 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import isEmpty from "lodash/isEmpty"; -import set from "lodash/set"; -import pickBy from "lodash/pickBy"; import isArray from "lodash/isArray"; +import isEmpty from "lodash/isEmpty"; +import pickBy from "lodash/pickBy"; +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // base class -import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; -// helpers +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; -// types -import { IIssueRootStore } from "../root.store"; +import { IssueFiltersService } from "services/issue_filter.service"; import { IIssueFilterOptions, IIssueDisplayFilterOptions, @@ -17,10 +15,12 @@ import { IIssueFilters, TIssueParams, } from "@plane/types"; +import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; +// helpers +// types +import { IIssueRootStore } from "../root.store"; // constants -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; // services -import { IssueFiltersService } from "services/issue_filter.service"; export interface IArchivedIssuesFilter { // observables diff --git a/web/store/issue/archived/issue.store.ts b/web/store/issue/archived/issue.store.ts index a0b26eb8b..06aa9d29a 100644 --- a/web/store/issue/archived/issue.store.ts +++ b/web/store/issue/archived/issue.store.ts @@ -1,13 +1,13 @@ -import { action, observable, makeObservable, computed, runInAction } from "mobx"; -import set from "lodash/set"; import pull from "lodash/pull"; +import set from "lodash/set"; +import { action, observable, makeObservable, computed, runInAction } from "mobx"; // base class +import { IssueArchiveService } from "services/issue"; +import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; import { IssueHelperStore } from "../helpers/issue-helper.store"; // services -import { IssueArchiveService } from "services/issue"; // types import { IIssueRootStore } from "../root.store"; -import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; export interface IArchivedIssues { // observable diff --git a/web/store/issue/cycle/filter.store.ts b/web/store/issue/cycle/filter.store.ts index 5d8c2a6b8..c4a345c47 100644 --- a/web/store/issue/cycle/filter.store.ts +++ b/web/store/issue/cycle/filter.store.ts @@ -1,14 +1,12 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import isEmpty from "lodash/isEmpty"; -import set from "lodash/set"; -import pickBy from "lodash/pickBy"; import isArray from "lodash/isArray"; +import isEmpty from "lodash/isEmpty"; +import pickBy from "lodash/pickBy"; +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // base class -import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; -// helpers +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; -// types -import { IIssueRootStore } from "../root.store"; +import { IssueFiltersService } from "services/issue_filter.service"; import { IIssueFilterOptions, IIssueDisplayFilterOptions, @@ -17,10 +15,12 @@ import { IIssueFilters, TIssueParams, } from "@plane/types"; +import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; +// helpers +// types +import { IIssueRootStore } from "../root.store"; // constants -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; // services -import { IssueFiltersService } from "services/issue_filter.service"; export interface ICycleIssuesFilter { // observables diff --git a/web/store/issue/cycle/issue.store.ts b/web/store/issue/cycle/issue.store.ts index 61b280da9..ef6e1872d 100644 --- a/web/store/issue/cycle/issue.store.ts +++ b/web/store/issue/cycle/issue.store.ts @@ -1,17 +1,17 @@ -import { action, observable, makeObservable, computed, runInAction } from "mobx"; -import set from "lodash/set"; -import update from "lodash/update"; import concat from "lodash/concat"; import pull from "lodash/pull"; +import set from "lodash/set"; import uniq from "lodash/uniq"; +import update from "lodash/update"; +import { action, observable, makeObservable, computed, runInAction } from "mobx"; // base class -import { IssueHelperStore } from "../helpers/issue-helper.store"; // services -import { IssueService } from "services/issue"; import { CycleService } from "services/cycle.service"; +import { IssueService } from "services/issue"; // types -import { IIssueRootStore } from "../root.store"; import { TIssue, TSubGroupedIssues, TGroupedIssues, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types"; +import { IssueHelperStore } from "../helpers/issue-helper.store"; +import { IIssueRootStore } from "../root.store"; export const ACTIVE_CYCLE_ISSUES = "ACTIVE_CYCLE_ISSUES"; diff --git a/web/store/issue/draft/filter.store.ts b/web/store/issue/draft/filter.store.ts index cc58a7755..51b8d9bc7 100644 --- a/web/store/issue/draft/filter.store.ts +++ b/web/store/issue/draft/filter.store.ts @@ -1,14 +1,12 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import isEmpty from "lodash/isEmpty"; -import set from "lodash/set"; -import pickBy from "lodash/pickBy"; import isArray from "lodash/isArray"; +import isEmpty from "lodash/isEmpty"; +import pickBy from "lodash/pickBy"; +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // base class -import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; -// helpers +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; -// types -import { IIssueRootStore } from "../root.store"; +import { IssueFiltersService } from "services/issue_filter.service"; import { IIssueFilterOptions, IIssueDisplayFilterOptions, @@ -17,10 +15,12 @@ import { IIssueFilters, TIssueParams, } from "@plane/types"; +import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; +// helpers +// types +import { IIssueRootStore } from "../root.store"; // constants -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; // services -import { IssueFiltersService } from "services/issue_filter.service"; export interface IDraftIssuesFilter { // observables diff --git a/web/store/issue/draft/issue.store.ts b/web/store/issue/draft/issue.store.ts index a06213eb0..67dcf2729 100644 --- a/web/store/issue/draft/issue.store.ts +++ b/web/store/issue/draft/issue.store.ts @@ -1,16 +1,16 @@ -import { action, observable, makeObservable, computed, runInAction } from "mobx"; -import set from "lodash/set"; -import update from "lodash/update"; -import uniq from "lodash/uniq"; import concat from "lodash/concat"; import pull from "lodash/pull"; +import set from "lodash/set"; +import uniq from "lodash/uniq"; +import update from "lodash/update"; +import { action, observable, makeObservable, computed, runInAction } from "mobx"; // base class -import { IssueHelperStore } from "../helpers/issue-helper.store"; // services import { IssueDraftService } from "services/issue/issue_draft.service"; // types -import { IIssueRootStore } from "../root.store"; import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; +import { IssueHelperStore } from "../helpers/issue-helper.store"; +import { IIssueRootStore } from "../root.store"; export interface IDraftIssues { // observable diff --git a/web/store/issue/helpers/issue-filter-helper.store.ts b/web/store/issue/helpers/issue-filter-helper.store.ts index baac4a2ad..2921e9ca8 100644 --- a/web/store/issue/helpers/issue-filter-helper.store.ts +++ b/web/store/issue/helpers/issue-filter-helper.store.ts @@ -1,5 +1,9 @@ import isEmpty from "lodash/isEmpty"; // types +// constants +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +// lib +import { storage } from "lib/local-storage"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, @@ -10,10 +14,6 @@ import { TIssueParams, TStaticViewTypes, } from "@plane/types"; -// constants -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; -// lib -import { storage } from "lib/local-storage"; interface ILocalStoreIssueFilters { key: EIssuesStoreType; diff --git a/web/store/issue/helpers/issue-helper.store.ts b/web/store/issue/helpers/issue-helper.store.ts index a267ac9c8..235f65e7c 100644 --- a/web/store/issue/helpers/issue-helper.store.ts +++ b/web/store/issue/helpers/issue-helper.store.ts @@ -1,16 +1,16 @@ -import orderBy from "lodash/orderBy"; import get from "lodash/get"; import indexOf from "lodash/indexOf"; import isEmpty from "lodash/isEmpty"; +import orderBy from "lodash/orderBy"; import values from "lodash/values"; // types -import { TIssue, TIssueMap, TIssueGroupByOptions, TIssueOrderByOptions } from "@plane/types"; -import { IIssueRootStore } from "../root.store"; // constants import { ISSUE_PRIORITIES } from "constants/issue"; import { STATE_GROUPS } from "constants/state"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { TIssue, TIssueMap, TIssueGroupByOptions, TIssueOrderByOptions } from "@plane/types"; +import { IIssueRootStore } from "../root.store"; export type TIssueDisplayFilterOptions = Exclude | "target_date"; diff --git a/web/store/issue/issue-details/activity.store.ts b/web/store/issue/issue-details/activity.store.ts index efa181c95..5afb6d8e4 100644 --- a/web/store/issue/issue-details/activity.store.ts +++ b/web/store/issue/issue-details/activity.store.ts @@ -1,14 +1,14 @@ -import { action, makeObservable, observable, runInAction } from "mobx"; +import concat from "lodash/concat"; import set from "lodash/set"; import sortBy from "lodash/sortBy"; -import update from "lodash/update"; -import concat from "lodash/concat"; import uniq from "lodash/uniq"; +import update from "lodash/update"; +import { action, makeObservable, observable, runInAction } from "mobx"; // services import { IssueActivityService } from "services/issue"; // types -import { IIssueDetail } from "./root.store"; import { TIssueActivityComment, TIssueActivity, TIssueActivityMap, TIssueActivityIdMap } from "@plane/types"; +import { IIssueDetail } from "./root.store"; export type TActivityLoader = "fetch" | "mutate" | undefined; diff --git a/web/store/issue/issue-details/attachment.store.ts b/web/store/issue/issue-details/attachment.store.ts index 5341058c1..47e95b437 100644 --- a/web/store/issue/issue-details/attachment.store.ts +++ b/web/store/issue/issue-details/attachment.store.ts @@ -1,14 +1,14 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import set from "lodash/set"; -import update from "lodash/update"; import concat from "lodash/concat"; -import uniq from "lodash/uniq"; import pull from "lodash/pull"; +import set from "lodash/set"; +import uniq from "lodash/uniq"; +import update from "lodash/update"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // services import { IssueAttachmentService } from "services/issue"; // types -import { IIssueDetail } from "./root.store"; import { TIssueAttachment, TIssueAttachmentMap, TIssueAttachmentIdMap } from "@plane/types"; +import { IIssueDetail } from "./root.store"; export interface IIssueAttachmentStoreActions { addAttachments: (issueId: string, attachments: TIssueAttachment[]) => void; diff --git a/web/store/issue/issue-details/comment.store.ts b/web/store/issue/issue-details/comment.store.ts index 4336971de..434be2778 100644 --- a/web/store/issue/issue-details/comment.store.ts +++ b/web/store/issue/issue-details/comment.store.ts @@ -1,14 +1,14 @@ -import { action, makeObservable, observable, runInAction } from "mobx"; -import set from "lodash/set"; -import update from "lodash/update"; import concat from "lodash/concat"; -import uniq from "lodash/uniq"; import pull from "lodash/pull"; +import set from "lodash/set"; +import uniq from "lodash/uniq"; +import update from "lodash/update"; +import { action, makeObservable, observable, runInAction } from "mobx"; // services import { IssueCommentService } from "services/issue"; // types -import { IIssueDetail } from "./root.store"; import { TIssueComment, TIssueCommentMap, TIssueCommentIdMap } from "@plane/types"; +import { IIssueDetail } from "./root.store"; export type TCommentLoader = "fetch" | "create" | "update" | "delete" | "mutate" | undefined; diff --git a/web/store/issue/issue-details/comment_reaction.store.ts b/web/store/issue/issue-details/comment_reaction.store.ts index 59adeef62..832f798d9 100644 --- a/web/store/issue/issue-details/comment_reaction.store.ts +++ b/web/store/issue/issue-details/comment_reaction.store.ts @@ -1,16 +1,16 @@ -import { action, makeObservable, observable, runInAction } from "mobx"; -import set from "lodash/set"; -import update from "lodash/update"; import concat from "lodash/concat"; import find from "lodash/find"; import pull from "lodash/pull"; +import set from "lodash/set"; +import update from "lodash/update"; +import { action, makeObservable, observable, runInAction } from "mobx"; // services -import { IssueReactionService } from "services/issue"; // types -import { IIssueDetail } from "./root.store"; -import { TIssueCommentReaction, TIssueCommentReactionIdMap, TIssueCommentReactionMap } from "@plane/types"; // helpers import { groupReactions } from "helpers/emoji.helper"; +import { IssueReactionService } from "services/issue"; +import { TIssueCommentReaction, TIssueCommentReactionIdMap, TIssueCommentReactionMap } from "@plane/types"; +import { IIssueDetail } from "./root.store"; export interface IIssueCommentReactionStoreActions { // actions diff --git a/web/store/issue/issue-details/issue.store.ts b/web/store/issue/issue-details/issue.store.ts index f42c13376..ba1d9b752 100644 --- a/web/store/issue/issue-details/issue.store.ts +++ b/web/store/issue/issue-details/issue.store.ts @@ -1,9 +1,9 @@ import { makeObservable } from "mobx"; // services +import { computedFn } from "mobx-utils"; import { IssueArchiveService, IssueDraftService, IssueService } from "services/issue"; // types import { TIssue } from "@plane/types"; -import { computedFn } from "mobx-utils"; import { IIssueDetail } from "./root.store"; export interface IIssueStoreActions { diff --git a/web/store/issue/issue-details/link.store.ts b/web/store/issue/issue-details/link.store.ts index 81d13438c..1cfd47c3f 100644 --- a/web/store/issue/issue-details/link.store.ts +++ b/web/store/issue/issue-details/link.store.ts @@ -1,10 +1,10 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // services import { IssueService } from "services/issue"; // types -import { IIssueDetail } from "./root.store"; import { TIssueLink, TIssueLinkMap, TIssueLinkIdMap } from "@plane/types"; +import { IIssueDetail } from "./root.store"; export interface IIssueLinkStoreActions { addLinks: (issueId: string, links: TIssueLink[]) => void; diff --git a/web/store/issue/issue-details/reaction.store.ts b/web/store/issue/issue-details/reaction.store.ts index 6282ac40e..a32ba6eca 100644 --- a/web/store/issue/issue-details/reaction.store.ts +++ b/web/store/issue/issue-details/reaction.store.ts @@ -1,16 +1,16 @@ -import { action, makeObservable, observable, runInAction } from "mobx"; -import set from "lodash/set"; -import update from "lodash/update"; import concat from "lodash/concat"; import find from "lodash/find"; import pull from "lodash/pull"; +import set from "lodash/set"; +import update from "lodash/update"; +import { action, makeObservable, observable, runInAction } from "mobx"; // services -import { IssueReactionService } from "services/issue"; // types -import { IIssueDetail } from "./root.store"; -import { TIssueReaction, TIssueReactionMap, TIssueReactionIdMap } from "@plane/types"; // helpers import { groupReactions } from "helpers/emoji.helper"; +import { IssueReactionService } from "services/issue"; +import { TIssueReaction, TIssueReactionMap, TIssueReactionIdMap } from "@plane/types"; +import { IIssueDetail } from "./root.store"; export interface IIssueReactionStoreActions { // actions diff --git a/web/store/issue/issue-details/relation.store.ts b/web/store/issue/issue-details/relation.store.ts index da729540e..fafa4ad4d 100644 --- a/web/store/issue/issue-details/relation.store.ts +++ b/web/store/issue/issue-details/relation.store.ts @@ -1,10 +1,10 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // services import { IssueRelationService } from "services/issue"; // types -import { IIssueDetail } from "./root.store"; import { TIssueRelationIdMap, TIssueRelationMap, TIssueRelationTypes, TIssueRelation, TIssue } from "@plane/types"; +import { IIssueDetail } from "./root.store"; export interface IIssueRelationStoreActions { // actions diff --git a/web/store/issue/issue-details/root.store.ts b/web/store/issue/issue-details/root.store.ts index db5dab307..be77efcd1 100644 --- a/web/store/issue/issue-details/root.store.ts +++ b/web/store/issue/issue-details/root.store.ts @@ -1,20 +1,5 @@ import { action, computed, makeObservable, observable } from "mobx"; // types -import { IIssueRootStore } from "../root.store"; -import { IIssueStore, IssueStore, IIssueStoreActions } from "./issue.store"; -import { IIssueReactionStore, IssueReactionStore, IIssueReactionStoreActions } from "./reaction.store"; -import { IIssueLinkStore, IssueLinkStore, IIssueLinkStoreActions } from "./link.store"; -import { IIssueSubscriptionStore, IssueSubscriptionStore, IIssueSubscriptionStoreActions } from "./subscription.store"; -import { IIssueAttachmentStore, IssueAttachmentStore, IIssueAttachmentStoreActions } from "./attachment.store"; -import { IIssueSubIssuesStore, IssueSubIssuesStore, IIssueSubIssuesStoreActions } from "./sub_issues.store"; -import { IIssueRelationStore, IssueRelationStore, IIssueRelationStoreActions } from "./relation.store"; -import { IIssueActivityStore, IssueActivityStore, IIssueActivityStoreActions, TActivityLoader } from "./activity.store"; -import { IIssueCommentStore, IssueCommentStore, IIssueCommentStoreActions, TCommentLoader } from "./comment.store"; -import { - IIssueCommentReactionStore, - IssueCommentReactionStore, - IIssueCommentReactionStoreActions, -} from "./comment_reaction.store"; import { TIssue, TIssueAttachment, @@ -24,6 +9,21 @@ import { TIssueReaction, TIssueRelationTypes, } from "@plane/types"; +import { IIssueRootStore } from "../root.store"; +import { IIssueActivityStore, IssueActivityStore, IIssueActivityStoreActions, TActivityLoader } from "./activity.store"; +import { IIssueAttachmentStore, IssueAttachmentStore, IIssueAttachmentStoreActions } from "./attachment.store"; +import { IIssueCommentStore, IssueCommentStore, IIssueCommentStoreActions, TCommentLoader } from "./comment.store"; +import { + IIssueCommentReactionStore, + IssueCommentReactionStore, + IIssueCommentReactionStoreActions, +} from "./comment_reaction.store"; +import { IIssueStore, IssueStore, IIssueStoreActions } from "./issue.store"; +import { IIssueLinkStore, IssueLinkStore, IIssueLinkStoreActions } from "./link.store"; +import { IIssueReactionStore, IssueReactionStore, IIssueReactionStoreActions } from "./reaction.store"; +import { IIssueRelationStore, IssueRelationStore, IIssueRelationStoreActions } from "./relation.store"; +import { IIssueSubIssuesStore, IssueSubIssuesStore, IIssueSubIssuesStoreActions } from "./sub_issues.store"; +import { IIssueSubscriptionStore, IssueSubscriptionStore, IIssueSubscriptionStoreActions } from "./subscription.store"; export type TPeekIssue = { workspaceSlug: string; diff --git a/web/store/issue/issue-details/sub_issues.store.ts b/web/store/issue/issue-details/sub_issues.store.ts index cfa1be12e..87ec58930 100644 --- a/web/store/issue/issue-details/sub_issues.store.ts +++ b/web/store/issue/issue-details/sub_issues.store.ts @@ -1,12 +1,11 @@ -import { action, makeObservable, observable, runInAction } from "mobx"; -import set from "lodash/set"; import concat from "lodash/concat"; -import update from "lodash/update"; import pull from "lodash/pull"; +import set from "lodash/set"; +import update from "lodash/update"; +import { action, makeObservable, observable, runInAction } from "mobx"; // services import { IssueService } from "services/issue"; // types -import { IIssueDetail } from "./root.store"; import { TIssue, TIssueSubIssues, @@ -14,6 +13,7 @@ import { TIssueSubIssuesIdMap, TSubIssuesStateDistribution, } from "@plane/types"; +import { IIssueDetail } from "./root.store"; export interface IIssueSubIssuesStoreActions { fetchSubIssues: (workspaceSlug: string, projectId: string, parentIssueId: string) => Promise; diff --git a/web/store/issue/issue-details/subscription.store.ts b/web/store/issue/issue-details/subscription.store.ts index 276c952f4..48b353e72 100644 --- a/web/store/issue/issue-details/subscription.store.ts +++ b/web/store/issue/issue-details/subscription.store.ts @@ -1,5 +1,5 @@ -import { action, makeObservable, observable, runInAction } from "mobx"; import set from "lodash/set"; +import { action, makeObservable, observable, runInAction } from "mobx"; // services import { NotificationService } from "services/notification.service"; // types diff --git a/web/store/issue/issue.store.ts b/web/store/issue/issue.store.ts index cbda505ff..635c75b24 100644 --- a/web/store/issue/issue.store.ts +++ b/web/store/issue/issue.store.ts @@ -1,12 +1,12 @@ -import set from "lodash/set"; import isEmpty from "lodash/isEmpty"; +import set from "lodash/set"; // store import { action, makeObservable, observable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; // types +import { IssueService } from "services/issue"; import { TIssue } from "@plane/types"; //services -import { IssueService } from "services/issue"; export type IIssueStore = { // observables diff --git a/web/store/issue/issue_calendar_view.store.ts b/web/store/issue/issue_calendar_view.store.ts index ac4a60809..98181d730 100644 --- a/web/store/issue/issue_calendar_view.store.ts +++ b/web/store/issue/issue_calendar_view.store.ts @@ -1,9 +1,9 @@ import { observable, action, makeObservable, runInAction, computed } from "mobx"; // helpers +import { ICalendarPayload, ICalendarWeek } from "components/issues"; import { generateCalendarData } from "helpers/calendar.helper"; // types -import { ICalendarPayload, ICalendarWeek } from "components/issues"; import { getWeekNumberOfDate } from "helpers/date-time.helper"; export interface ICalendarStore { diff --git a/web/store/issue/issue_gantt_view.store.ts b/web/store/issue/issue_gantt_view.store.ts index b087554dd..e478e8649 100644 --- a/web/store/issue/issue_gantt_view.store.ts +++ b/web/store/issue/issue_gantt_view.store.ts @@ -1,9 +1,9 @@ import { action, makeObservable, observable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; // helpers +import { ChartDataType, TGanttViews } from "components/gantt-chart"; import { currentViewDataWithView } from "components/gantt-chart/data"; // types -import { ChartDataType, TGanttViews } from "components/gantt-chart"; export interface IGanttStore { // observables diff --git a/web/store/issue/module/filter.store.ts b/web/store/issue/module/filter.store.ts index c353059ef..c34a31b23 100644 --- a/web/store/issue/module/filter.store.ts +++ b/web/store/issue/module/filter.store.ts @@ -1,14 +1,12 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import isEmpty from "lodash/isEmpty"; -import set from "lodash/set"; -import pickBy from "lodash/pickBy"; import isArray from "lodash/isArray"; +import isEmpty from "lodash/isEmpty"; +import pickBy from "lodash/pickBy"; +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // base class -import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; -// helpers +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; -// types -import { IIssueRootStore } from "../root.store"; +import { IssueFiltersService } from "services/issue_filter.service"; import { IIssueFilterOptions, IIssueDisplayFilterOptions, @@ -17,10 +15,12 @@ import { IIssueFilters, TIssueParams, } from "@plane/types"; +import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; +// helpers +// types +import { IIssueRootStore } from "../root.store"; // constants -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; // services -import { IssueFiltersService } from "services/issue_filter.service"; export interface IModuleIssuesFilter { // observables diff --git a/web/store/issue/module/issue.store.ts b/web/store/issue/module/issue.store.ts index 9e6ad3f49..668473195 100644 --- a/web/store/issue/module/issue.store.ts +++ b/web/store/issue/module/issue.store.ts @@ -1,17 +1,17 @@ -import { action, observable, makeObservable, computed, runInAction } from "mobx"; -import set from "lodash/set"; -import update from "lodash/update"; import concat from "lodash/concat"; import pull from "lodash/pull"; +import set from "lodash/set"; import uniq from "lodash/uniq"; +import update from "lodash/update"; +import { action, observable, makeObservable, computed, runInAction } from "mobx"; // base class -import { IssueHelperStore } from "../helpers/issue-helper.store"; // services import { IssueService } from "services/issue"; import { ModuleService } from "services/module.service"; // types -import { IIssueRootStore } from "../root.store"; import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; +import { IssueHelperStore } from "../helpers/issue-helper.store"; +import { IIssueRootStore } from "../root.store"; export interface IModuleIssues { // observable diff --git a/web/store/issue/profile/filter.store.ts b/web/store/issue/profile/filter.store.ts index 658980082..c7ebc378a 100644 --- a/web/store/issue/profile/filter.store.ts +++ b/web/store/issue/profile/filter.store.ts @@ -1,14 +1,12 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import isEmpty from "lodash/isEmpty"; -import set from "lodash/set"; -import pickBy from "lodash/pickBy"; import isArray from "lodash/isArray"; +import isEmpty from "lodash/isEmpty"; +import pickBy from "lodash/pickBy"; +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // base class -import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; -// helpers +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; -// types -import { IIssueRootStore } from "../root.store"; +import { IssueFiltersService } from "services/issue_filter.service"; import { IIssueFilterOptions, IIssueDisplayFilterOptions, @@ -17,10 +15,12 @@ import { IIssueFilters, TIssueParams, } from "@plane/types"; +import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; +// helpers +// types +import { IIssueRootStore } from "../root.store"; // constants -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; // services -import { IssueFiltersService } from "services/issue_filter.service"; export interface IProfileIssuesFilter { // observables diff --git a/web/store/issue/profile/issue.store.ts b/web/store/issue/profile/issue.store.ts index c39b33a80..39a37a2cf 100644 --- a/web/store/issue/profile/issue.store.ts +++ b/web/store/issue/profile/issue.store.ts @@ -1,13 +1,13 @@ -import { action, observable, makeObservable, computed, runInAction } from "mobx"; -import set from "lodash/set"; import pull from "lodash/pull"; +import set from "lodash/set"; +import { action, observable, makeObservable, computed, runInAction } from "mobx"; // base class +import { UserService } from "services/user.service"; +import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; import { IssueHelperStore } from "../helpers/issue-helper.store"; // services -import { UserService } from "services/user.service"; // types import { IIssueRootStore } from "../root.store"; -import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; interface IProfileIssueTabTypes { [key: string]: string[]; diff --git a/web/store/issue/project-views/filter.store.ts b/web/store/issue/project-views/filter.store.ts index c7c8988b1..27c980360 100644 --- a/web/store/issue/project-views/filter.store.ts +++ b/web/store/issue/project-views/filter.store.ts @@ -1,14 +1,12 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import isEmpty from "lodash/isEmpty"; -import set from "lodash/set"; -import pickBy from "lodash/pickBy"; import isArray from "lodash/isArray"; +import isEmpty from "lodash/isEmpty"; +import pickBy from "lodash/pickBy"; +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // base class -import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; -// helpers +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; -// types -import { IIssueRootStore } from "../root.store"; +import { ViewService } from "services/view.service"; import { IIssueFilterOptions, IIssueDisplayFilterOptions, @@ -17,10 +15,12 @@ import { IIssueFilters, TIssueParams, } from "@plane/types"; +import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; +// helpers +// types +import { IIssueRootStore } from "../root.store"; // constants -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; // services -import { ViewService } from "services/view.service"; export interface IProjectViewIssuesFilter { // observables diff --git a/web/store/issue/project-views/issue.store.ts b/web/store/issue/project-views/issue.store.ts index b85465ec8..012d6ebe8 100644 --- a/web/store/issue/project-views/issue.store.ts +++ b/web/store/issue/project-views/issue.store.ts @@ -1,13 +1,13 @@ -import { action, observable, makeObservable, computed, runInAction } from "mobx"; -import set from "lodash/set"; import pull from "lodash/pull"; +import set from "lodash/set"; +import { action, observable, makeObservable, computed, runInAction } from "mobx"; // base class +import { IssueService } from "services/issue/issue.service"; +import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; import { IssueHelperStore } from "../helpers/issue-helper.store"; // services -import { IssueService } from "services/issue/issue.service"; // types import { IIssueRootStore } from "../root.store"; -import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; export interface IProjectViewIssues { // observable diff --git a/web/store/issue/project/filter.store.ts b/web/store/issue/project/filter.store.ts index f18654cde..d5c353487 100644 --- a/web/store/issue/project/filter.store.ts +++ b/web/store/issue/project/filter.store.ts @@ -1,14 +1,12 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import isEmpty from "lodash/isEmpty"; -import set from "lodash/set"; -import pickBy from "lodash/pickBy"; import isArray from "lodash/isArray"; +import isEmpty from "lodash/isEmpty"; +import pickBy from "lodash/pickBy"; +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // base class -import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; -// helpers +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; -// types -import { IIssueRootStore } from "../root.store"; +import { IssueFiltersService } from "services/issue_filter.service"; import { IIssueFilterOptions, IIssueDisplayFilterOptions, @@ -17,10 +15,12 @@ import { IIssueFilters, TIssueParams, } from "@plane/types"; +import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; +// helpers +// types +import { IIssueRootStore } from "../root.store"; // constants -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; // services -import { IssueFiltersService } from "services/issue_filter.service"; export interface IProjectIssuesFilter { // observables @@ -189,7 +189,6 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj updatedDisplayFilters.group_by = "state"; } - runInAction(() => { Object.keys(updatedDisplayFilters).forEach((_key) => { set( diff --git a/web/store/issue/project/issue.store.ts b/web/store/issue/project/issue.store.ts index f3ee94783..080b8cee6 100644 --- a/web/store/issue/project/issue.store.ts +++ b/web/store/issue/project/issue.store.ts @@ -1,15 +1,15 @@ -import { action, makeObservable, observable, runInAction, computed } from "mobx"; +import concat from "lodash/concat"; +import pull from "lodash/pull"; import set from "lodash/set"; import update from "lodash/update"; -import pull from "lodash/pull"; -import concat from "lodash/concat"; +import { action, makeObservable, observable, runInAction, computed } from "mobx"; // base class +import { IssueService, IssueArchiveService } from "services/issue"; +import { TIssue, TGroupedIssues, TSubGroupedIssues, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types"; import { IssueHelperStore } from "../helpers/issue-helper.store"; // services -import { IssueService, IssueArchiveService } from "services/issue"; // types import { IIssueRootStore } from "../root.store"; -import { TIssue, TGroupedIssues, TSubGroupedIssues, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types"; export interface IProjectIssues { // observable diff --git a/web/store/issue/root.store.ts b/web/store/issue/root.store.ts index def91d200..a9dde82ae 100644 --- a/web/store/issue/root.store.ts +++ b/web/store/issue/root.store.ts @@ -1,28 +1,28 @@ -import { autorun, makeObservable, observable } from "mobx"; import isEmpty from "lodash/isEmpty"; +import { autorun, makeObservable, observable } from "mobx"; // root store +import { IWorkspaceMembership } from "store/member/workspace-member.store"; +import { ICycle, IIssueLabel, IModule, IProject, IState, IUserLite } from "@plane/types"; import { RootStore } from "../root.store"; import { IStateStore, StateStore } from "../state.store"; // issues data store -import { ICycle, IIssueLabel, IModule, IProject, IState, IUserLite } from "@plane/types"; -import { IIssueStore, IssueStore } from "./issue.store"; +import { IArchivedIssuesFilter, ArchivedIssuesFilter, IArchivedIssues, ArchivedIssues } from "./archived"; +import { ICycleIssuesFilter, CycleIssuesFilter, ICycleIssues, CycleIssues } from "./cycle"; +import { IDraftIssuesFilter, DraftIssuesFilter, IDraftIssues, DraftIssues } from "./draft"; import { IIssueDetail, IssueDetail } from "./issue-details/root.store"; -import { IWorkspaceIssuesFilter, WorkspaceIssuesFilter, IWorkspaceIssues, WorkspaceIssues } from "./workspace"; +import { IIssueStore, IssueStore } from "./issue.store"; +import { ICalendarStore, CalendarStore } from "./issue_calendar_view.store"; +import { IIssueKanBanViewStore, IssueKanBanViewStore } from "./issue_kanban_view.store"; +import { IModuleIssuesFilter, ModuleIssuesFilter, IModuleIssues, ModuleIssues } from "./module"; import { IProfileIssuesFilter, ProfileIssuesFilter, IProfileIssues, ProfileIssues } from "./profile"; import { IProjectIssuesFilter, ProjectIssuesFilter, IProjectIssues, ProjectIssues } from "./project"; -import { ICycleIssuesFilter, CycleIssuesFilter, ICycleIssues, CycleIssues } from "./cycle"; -import { IModuleIssuesFilter, ModuleIssuesFilter, IModuleIssues, ModuleIssues } from "./module"; import { IProjectViewIssuesFilter, ProjectViewIssuesFilter, IProjectViewIssues, ProjectViewIssues, } from "./project-views"; -import { IArchivedIssuesFilter, ArchivedIssuesFilter, IArchivedIssues, ArchivedIssues } from "./archived"; -import { IDraftIssuesFilter, DraftIssuesFilter, IDraftIssues, DraftIssues } from "./draft"; -import { IIssueKanBanViewStore, IssueKanBanViewStore } from "./issue_kanban_view.store"; -import { ICalendarStore, CalendarStore } from "./issue_calendar_view.store"; -import { IWorkspaceMembership } from "store/member/workspace-member.store"; +import { IWorkspaceIssuesFilter, WorkspaceIssuesFilter, IWorkspaceIssues, WorkspaceIssues } from "./workspace"; export interface IIssueRootStore { currentUserId: string | undefined; diff --git a/web/store/issue/workspace/filter.store.ts b/web/store/issue/workspace/filter.store.ts index 76b861f4b..d6f1aba74 100644 --- a/web/store/issue/workspace/filter.store.ts +++ b/web/store/issue/workspace/filter.store.ts @@ -1,14 +1,12 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import isEmpty from "lodash/isEmpty"; -import set from "lodash/set"; -import pickBy from "lodash/pickBy"; import isArray from "lodash/isArray"; +import isEmpty from "lodash/isEmpty"; +import pickBy from "lodash/pickBy"; +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // base class -import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; -// helpers +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; -// types -import { IIssueRootStore } from "../root.store"; +import { WorkspaceService } from "services/workspace.service"; import { IIssueFilterOptions, IIssueDisplayFilterOptions, @@ -18,10 +16,12 @@ import { TIssueParams, TStaticViewTypes, } from "@plane/types"; +import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; +// helpers +// types +import { IIssueRootStore } from "../root.store"; // constants -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; // services -import { WorkspaceService } from "services/workspace.service"; type TWorkspaceFilters = "all-issues" | "assigned" | "created" | "subscribed" | string; export interface IWorkspaceIssuesFilter { diff --git a/web/store/issue/workspace/issue.store.ts b/web/store/issue/workspace/issue.store.ts index b7fe43b30..cc859755f 100644 --- a/web/store/issue/workspace/issue.store.ts +++ b/web/store/issue/workspace/issue.store.ts @@ -1,14 +1,14 @@ -import { action, observable, makeObservable, computed, runInAction } from "mobx"; -import set from "lodash/set"; import pull from "lodash/pull"; +import set from "lodash/set"; +import { action, observable, makeObservable, computed, runInAction } from "mobx"; // base class +import { IssueService, IssueArchiveService } from "services/issue"; +import { WorkspaceService } from "services/workspace.service"; +import { TIssue, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types"; import { IssueHelperStore } from "../helpers/issue-helper.store"; // services -import { WorkspaceService } from "services/workspace.service"; -import { IssueService, IssueArchiveService } from "services/issue"; // types import { IIssueRootStore } from "../root.store"; -import { TIssue, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types"; export interface IWorkspaceIssues { // observable diff --git a/web/store/label.store.ts b/web/store/label.store.ts index 769ef16a9..386676dfe 100644 --- a/web/store/label.store.ts +++ b/web/store/label.store.ts @@ -1,11 +1,11 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; import set from "lodash/set"; import sortBy from "lodash/sortBy"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; // services +import { buildTree } from "helpers/array.helper"; import { IssueLabelService } from "services/issue"; // helpers -import { buildTree } from "helpers/array.helper"; // types import { RootStore } from "store/root.store"; import { IIssueLabel, IIssueLabelTree } from "@plane/types"; diff --git a/web/store/member/index.ts b/web/store/member/index.ts index a7eba3971..d43398d0b 100644 --- a/web/store/member/index.ts +++ b/web/store/member/index.ts @@ -2,8 +2,8 @@ import { action, makeObservable, observable } from "mobx"; // types import { RootStore } from "store/root.store"; import { IUserLite } from "@plane/types"; -import { IWorkspaceMemberStore, WorkspaceMemberStore } from "./workspace-member.store"; import { IProjectMemberStore, ProjectMemberStore } from "./project-member.store"; +import { IWorkspaceMemberStore, WorkspaceMemberStore } from "./workspace-member.store"; export interface IMemberRootStore { // observables diff --git a/web/store/member/project-member.store.ts b/web/store/member/project-member.store.ts index 71e2e2dcd..6cb39e2ef 100644 --- a/web/store/member/project-member.store.ts +++ b/web/store/member/project-member.store.ts @@ -1,17 +1,17 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; import set from "lodash/set"; import sortBy from "lodash/sortBy"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; // services +import { EUserProjectRoles } from "constants/project"; import { ProjectMemberService } from "services/project"; // types +import { IRouterStore } from "store/application/router.store"; import { RootStore } from "store/root.store"; +import { IUserRootStore } from "store/user"; import { IProjectBulkAddFormData, IProjectMember, IProjectMembership, IUserLite } from "@plane/types"; // constants -import { EUserProjectRoles } from "constants/project"; import { IMemberRootStore } from "."; -import { IRouterStore } from "store/application/router.store"; -import { IUserRootStore } from "store/user"; interface IProjectMemberDetails { id: string; diff --git a/web/store/member/workspace-member.store.ts b/web/store/member/workspace-member.store.ts index 4a696bfd2..a901dccc1 100644 --- a/web/store/member/workspace-member.store.ts +++ b/web/store/member/workspace-member.store.ts @@ -1,17 +1,17 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; import set from "lodash/set"; import sortBy from "lodash/sortBy"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; // services +import { EUserWorkspaceRoles } from "constants/workspace"; import { WorkspaceService } from "services/workspace.service"; // types +import { IRouterStore } from "store/application/router.store"; import { RootStore } from "store/root.store"; +import { IUserRootStore } from "store/user"; import { IWorkspaceBulkInviteFormData, IWorkspaceMember, IWorkspaceMemberInvitation } from "@plane/types"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; -import { IRouterStore } from "store/application/router.store"; import { IMemberRootStore } from "."; -import { IUserRootStore } from "store/user"; export interface IWorkspaceMembership { id: string; diff --git a/web/store/mention.store.ts b/web/store/mention.store.ts index 872efeb41..5cfa0478a 100644 --- a/web/store/mention.store.ts +++ b/web/store/mention.store.ts @@ -1,6 +1,6 @@ +import { IMentionHighlight, IMentionSuggestion } from "@plane/lite-text-editor"; import { computed, makeObservable } from "mobx"; // editor -import { IMentionHighlight, IMentionSuggestion } from "@plane/lite-text-editor"; // types import { RootStore } from "store/root.store"; diff --git a/web/store/module.store.ts b/web/store/module.store.ts index 2b4522cd0..c7dcba79c 100644 --- a/web/store/module.store.ts +++ b/web/store/module.store.ts @@ -1,13 +1,13 @@ -import { action, computed, observable, makeObservable, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; import set from "lodash/set"; import sortBy from "lodash/sortBy"; +import { action, computed, observable, makeObservable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; // services -import { ProjectService } from "services/project"; import { ModuleService } from "services/module.service"; +import { ProjectService } from "services/project"; // types -import { IModule, ILinkDetails } from "@plane/types"; import { RootStore } from "store/root.store"; +import { IModule, ILinkDetails } from "@plane/types"; export interface IModuleStore { //Loaders diff --git a/web/store/page.store.ts b/web/store/page.store.ts index fa5970e49..ae416237f 100644 --- a/web/store/page.store.ts +++ b/web/store/page.store.ts @@ -1,7 +1,7 @@ import { action, makeObservable, observable, reaction, runInAction } from "mobx"; -import { IIssueLabel, IPage } from "@plane/types"; import { PageService } from "services/page.service"; +import { IIssueLabel, IPage } from "@plane/types"; import { RootStore } from "./root.store"; diff --git a/web/store/project-page.store.ts b/web/store/project-page.store.ts index 072605bc3..c16e8ab08 100644 --- a/web/store/project-page.store.ts +++ b/web/store/project-page.store.ts @@ -1,5 +1,6 @@ -import { makeObservable, observable, runInAction, action, computed } from "mobx"; +import { isThisWeek, isToday, isYesterday } from "date-fns"; import { set } from "lodash"; +import { makeObservable, observable, runInAction, action, computed } from "mobx"; // services import { PageService } from "services/page.service"; // store @@ -7,7 +8,6 @@ import { PageStore, IPageStore } from "store/page.store"; // types import { IPage, IRecentPages } from "@plane/types"; import { RootStore } from "./root.store"; -import { isThisWeek, isToday, isYesterday } from "date-fns"; export interface IProjectPageStore { loader: boolean; diff --git a/web/store/project/index.ts b/web/store/project/index.ts index 696b3c802..dff0db175 100644 --- a/web/store/project/index.ts +++ b/web/store/project/index.ts @@ -1,6 +1,6 @@ -import { IProjectStore, ProjectStore } from "./project.store"; -import { IProjectPublishStore, ProjectPublishStore } from "./project-publish.store"; import { RootStore } from "store/root.store"; +import { IProjectPublishStore, ProjectPublishStore } from "./project-publish.store"; +import { IProjectStore, ProjectStore } from "./project.store"; export interface IProjectRootStore { project: IProjectStore; diff --git a/web/store/project/project-publish.store.ts b/web/store/project/project-publish.store.ts index 3a94b8611..9be1cb48c 100644 --- a/web/store/project/project-publish.store.ts +++ b/web/store/project/project-publish.store.ts @@ -1,9 +1,9 @@ -import { observable, action, makeObservable, runInAction } from "mobx"; import set from "lodash/set"; +import { observable, action, makeObservable, runInAction } from "mobx"; // types +import { ProjectPublishService } from "services/project"; import { ProjectRootStore } from "./"; // services -import { ProjectPublishService } from "services/project"; export type TProjectPublishViews = "list" | "gantt" | "kanban" | "calendar" | "spreadsheet"; diff --git a/web/store/project/project.store.ts b/web/store/project/project.store.ts index 176c3a364..1b9220a2d 100644 --- a/web/store/project/project.store.ts +++ b/web/store/project/project.store.ts @@ -1,14 +1,14 @@ -import { observable, action, computed, makeObservable, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; +import { cloneDeep, update } from "lodash"; import set from "lodash/set"; import sortBy from "lodash/sortBy"; +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; // types -import { RootStore } from "../root.store"; -import { IProject } from "@plane/types"; -// services import { IssueLabelService, IssueService } from "services/issue"; import { ProjectService, ProjectStateService } from "services/project"; -import { cloneDeep, update } from "lodash"; +import { IProject } from "@plane/types"; +import { RootStore } from "../root.store"; +// services export interface IProjectStore { // observables searchQuery: string; diff --git a/web/store/root.store.ts b/web/store/root.store.ts index 3e0733249..298cd532e 100644 --- a/web/store/root.store.ts +++ b/web/store/root.store.ts @@ -1,23 +1,23 @@ import { enableStaticRendering } from "mobx-react-lite"; // root stores import { AppRootStore, IAppRootStore } from "./application"; -import { EventTrackerStore, IEventTrackerStore } from "./event-tracker.store"; -import { IProjectRootStore, ProjectRootStore } from "./project"; import { CycleStore, ICycleStore } from "./cycle.store"; -import { IProjectViewStore, ProjectViewStore } from "./project-view.store"; +import { DashboardStore, IDashboardStore } from "./dashboard.store"; +import { IEstimateStore, EstimateStore } from "./estimate.store"; +import { EventTrackerStore, IEventTrackerStore } from "./event-tracker.store"; +import { GlobalViewStore, IGlobalViewStore } from "./global-view.store"; +import { IInboxRootStore, InboxRootStore } from "./inbox/root.store"; +import { IssueRootStore, IIssueRootStore } from "./issue/root.store"; +import { ILabelStore, LabelStore } from "./label.store"; +import { IMemberRootStore, MemberRootStore } from "./member"; +import { IMentionStore, MentionStore } from "./mention.store"; import { IModuleStore, ModulesStore } from "./module.store"; +import { IProjectRootStore, ProjectRootStore } from "./project"; +import { IProjectViewStore, ProjectViewStore } from "./project-view.store"; +import { IStateStore, StateStore } from "./state.store"; import { IUserRootStore, UserRootStore } from "./user"; import { IWorkspaceRootStore, WorkspaceRootStore } from "./workspace"; -import { IssueRootStore, IIssueRootStore } from "./issue/root.store"; -import { IInboxRootStore, InboxRootStore } from "./inbox/root.store"; -import { IStateStore, StateStore } from "./state.store"; -import { IMemberRootStore, MemberRootStore } from "./member"; -import { IEstimateStore, EstimateStore } from "./estimate.store"; -import { GlobalViewStore, IGlobalViewStore } from "./global-view.store"; -import { IMentionStore, MentionStore } from "./mention.store"; -import { DashboardStore, IDashboardStore } from "./dashboard.store"; import { IProjectPageStore, ProjectPageStore } from "./project-page.store"; -import { ILabelStore, LabelStore } from "./label.store"; enableStaticRendering(typeof window === "undefined"); diff --git a/web/store/state.store.ts b/web/store/state.store.ts index 783a82ee2..df3496f39 100644 --- a/web/store/state.store.ts +++ b/web/store/state.store.ts @@ -1,15 +1,15 @@ -import { makeObservable, observable, computed, action, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; import groupBy from "lodash/groupBy"; import set from "lodash/set"; +import { makeObservable, observable, computed, action, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; // store +import { sortStates } from "helpers/state.helper"; +import { ProjectStateService } from "services/project"; +import { IState } from "@plane/types"; import { RootStore } from "./root.store"; // types -import { IState } from "@plane/types"; // services -import { ProjectStateService } from "services/project"; // helpers -import { sortStates } from "helpers/state.helper"; export interface IStateStore { //Loaders diff --git a/web/store/user/index.ts b/web/store/user/index.ts index ada2e6be7..1a94e16b3 100644 --- a/web/store/user/index.ts +++ b/web/store/user/index.ts @@ -1,7 +1,7 @@ import { action, observable, runInAction, makeObservable } from "mobx"; // services -import { UserService } from "services/user.service"; import { AuthService } from "services/auth.service"; +import { UserService } from "services/user.service"; // interfaces import { IUser, IUserSettings } from "@plane/types"; // store diff --git a/web/store/user/user-membership.store.ts b/web/store/user/user-membership.store.ts index b8bdbfac5..a1f5c1b81 100644 --- a/web/store/user/user-membership.store.ts +++ b/web/store/user/user-membership.store.ts @@ -1,6 +1,8 @@ -import { action, observable, runInAction, makeObservable, computed } from "mobx"; import { set } from "lodash"; +import { action, observable, runInAction, makeObservable, computed } from "mobx"; // services +import { EUserProjectRoles } from "constants/project"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { ProjectMemberService } from "services/project"; import { UserService } from "services/user.service"; import { WorkspaceService } from "services/workspace.service"; @@ -8,8 +10,6 @@ import { WorkspaceService } from "services/workspace.service"; import { IWorkspaceMemberMe, IProjectMember, IUserProjectsRole } from "@plane/types"; import { RootStore } from "../root.store"; // constants -import { EUserProjectRoles } from "constants/project"; -import { EUserWorkspaceRoles } from "constants/workspace"; export interface IUserMembershipStore { // observables diff --git a/web/store/workspace/api-token.store.ts b/web/store/workspace/api-token.store.ts index f0772933d..351ead561 100644 --- a/web/store/workspace/api-token.store.ts +++ b/web/store/workspace/api-token.store.ts @@ -2,9 +2,9 @@ import { action, observable, makeObservable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; import { APITokenService } from "services/api_token.service"; +import { IApiToken } from "@plane/types"; import { RootStore } from "../root.store"; // types -import { IApiToken } from "@plane/types"; export interface IApiTokenStore { // observables diff --git a/web/store/workspace/index.ts b/web/store/workspace/index.ts index 4020aaef7..863982e1a 100644 --- a/web/store/workspace/index.ts +++ b/web/store/workspace/index.ts @@ -1,13 +1,13 @@ -import { action, computed, observable, makeObservable, runInAction } from "mobx"; -import { RootStore } from "../root.store"; import set from "lodash/set"; -// types -import { IWorkspace } from "@plane/types"; -// services +import { action, computed, observable, makeObservable, runInAction } from "mobx"; import { WorkspaceService } from "services/workspace.service"; +import { IWorkspace } from "@plane/types"; +import { RootStore } from "../root.store"; +// types +// services // sub-stores -import { IWebhookStore, WebhookStore } from "./webhook.store"; import { ApiTokenStore, IApiTokenStore } from "./api-token.store"; +import { IWebhookStore, WebhookStore } from "./webhook.store"; export interface IWorkspaceRootStore { // observables diff --git a/web/store/workspace/webhook.store.ts b/web/store/workspace/webhook.store.ts index 5657f341e..256b41e38 100644 --- a/web/store/workspace/webhook.store.ts +++ b/web/store/workspace/webhook.store.ts @@ -1,8 +1,8 @@ // mobx import { action, observable, makeObservable, computed, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; -import { IWebhook } from "@plane/types"; import { WebhookService } from "services/webhook.service"; +import { IWebhook } from "@plane/types"; import { RootStore } from "../root.store"; export interface IWebhookStore { diff --git a/yarn.lock b/yarn.lock index 0a21fcee2..9518afff6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29,13 +29,6 @@ jsonpointer "^5.0.0" leven "^3.1.0" -"@babel/code-frame@7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" - integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== - dependencies: - "@babel/highlight" "^7.10.4" - "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.22.13", "@babel/code-frame@^7.23.5": version "7.23.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" @@ -269,7 +262,7 @@ "@babel/traverse" "^7.23.6" "@babel/types" "^7.23.6" -"@babel/highlight@^7.10.4", "@babel/highlight@^7.23.4": +"@babel/highlight@^7.23.4": version "7.23.4" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b" integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A== @@ -1288,37 +1281,7 @@ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== -"@eslint/eslintrc@^0.4.3": - version "0.4.3" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" - integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw== - dependencies: - ajv "^6.12.4" - debug "^4.1.1" - espree "^7.3.0" - globals "^13.9.0" - ignore "^4.0.6" - import-fresh "^3.2.1" - js-yaml "^3.13.1" - minimatch "^3.0.4" - strip-json-comments "^3.1.1" - -"@eslint/eslintrc@^1.4.1": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.4.1.tgz#af58772019a2d271b7e2d4c23ff4ddcba3ccfb3e" - integrity sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA== - dependencies: - ajv "^6.12.4" - debug "^4.3.2" - espree "^9.4.0" - globals "^13.19.0" - ignore "^5.2.0" - import-fresh "^3.2.1" - js-yaml "^4.1.0" - minimatch "^3.1.2" - strip-json-comments "^3.1.1" - -"@eslint/eslintrc@^2.0.1", "@eslint/eslintrc@^2.1.4": +"@eslint/eslintrc@^2.1.4": version "2.1.4" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== @@ -1333,15 +1296,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.36.0": - version "8.36.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.36.0.tgz#9837f768c03a1e4a30bd304a64fb8844f0e72efe" - integrity sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg== - -"@eslint/js@8.56.0": - version "8.56.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.56.0.tgz#ef20350fec605a7f7035a01764731b2de0f3782b" - integrity sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A== +"@eslint/js@8.57.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" + integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== "@floating-ui/core@^1.4.2": version "1.5.2" @@ -1399,38 +1357,24 @@ redux "^4.2.1" use-memo-one "^1.1.3" -"@humanwhocodes/config-array@^0.11.13", "@humanwhocodes/config-array@^0.11.8": - version "0.11.13" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297" - integrity sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ== +"@humanwhocodes/config-array@^0.11.14": + version "0.11.14" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" + integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== dependencies: - "@humanwhocodes/object-schema" "^2.0.1" - debug "^4.1.1" + "@humanwhocodes/object-schema" "^2.0.2" + debug "^4.3.1" minimatch "^3.0.5" -"@humanwhocodes/config-array@^0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" - integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg== - dependencies: - "@humanwhocodes/object-schema" "^1.2.0" - debug "^4.1.1" - minimatch "^3.0.4" - "@humanwhocodes/module-importer@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/object-schema@^1.2.0": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" - integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== - -"@humanwhocodes/object-schema@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz#e5211452df060fa8522b55c7b3c0c4d1981cb044" - integrity sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw== +"@humanwhocodes/object-schema@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917" + integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw== "@hypnosphi/create-react-context@^0.3.1": version "0.3.1" @@ -1591,33 +1535,12 @@ resolved "https://registry.yarnpkg.com/@next/env/-/env-14.0.4.tgz#d5cda0c4a862d70ae760e58c0cd96a8899a2e49a" integrity sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ== -"@next/eslint-plugin-next@12.2.2": - version "12.2.2" - resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-12.2.2.tgz#b4a22c06b6454068b54cc44502168d90fbb29a6d" - integrity sha512-XOi0WzJhGH3Lk51SkSu9eZxF+IY1ZZhWcJTIGBycAbWU877IQa6+6KxMATWCOs7c+bmp6Sd8KywXJaDRxzu0JA== +"@next/eslint-plugin-next@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-14.1.0.tgz#29b041233fac7417e22eefa4146432d5cd910820" + integrity sha512-x4FavbNEeXx/baD/zC/SdrvkjSby8nBn8KcCREqk6UuwvwoAPZmaV8TFCAuo/cpovBRTIY67mHhe86MQQm/68Q== dependencies: - glob "7.1.7" - -"@next/eslint-plugin-next@13.0.0": - version "13.0.0" - resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-13.0.0.tgz#cf3d799b21671554c1f5889c01d2513afb9973cd" - integrity sha512-z+gnX4Zizatqatc6f4CQrcC9oN8Us3Vrq/OLyc98h7K/eWctrnV91zFZodmJHUjx0cITY8uYM7LXD7IdYkg3kg== - dependencies: - glob "7.1.7" - -"@next/eslint-plugin-next@13.2.1": - version "13.2.1" - resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-13.2.1.tgz#58dea4d53c0adfc59c10195f51eb8d3575fce414" - integrity sha512-r0i5rcO6SMAZtqiGarUVMr3k256X0R0j6pEkKg4PxqUW+hG0qgMxRVAJsuoRG5OBFkCOlSfWZJ0mP9fQdCcyNg== - dependencies: - glob "7.1.7" - -"@next/eslint-plugin-next@13.2.4": - version "13.2.4" - resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-13.2.4.tgz#3e124cd10ce24dab5d3448ce04104b4f1f4c6ca7" - integrity sha512-ck1lI+7r1mMJpqLNa3LJ5pxCfOB1lfJncKmRJeJxcJqcngaFwylreLP7da6Rrjr6u2gVRTfmnkSkjc80IiQCwQ== - dependencies: - glob "7.1.7" + glob "10.3.10" "@next/swc-darwin-arm64@14.0.4": version "14.0.4" @@ -2199,10 +2122,10 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.1.tgz#d146db7a5949e10837b323ce933ed882ac878262" integrity sha512-PyJsSsafjmIhVgaI1Zdj7m8BB8mMckFah/xbpplObyHfiXzKcI5UOUXRyOdHW7nz4DpMCuzLnF7v5IWHenCwYA== -"@rushstack/eslint-patch@^1.1.3": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.6.1.tgz#9ab8f811930d7af3e3d549183a50884f9eb83f36" - integrity sha512-UY+FGM/2jjMkzQLn8pxcHGMaVLh9aEitG3zY2CiY7XHdLiz3bZOwa6oDxNqEMv7zZkV+cj5DOdz0cQ1BP5Hjgw== +"@rushstack/eslint-patch@^1.3.3": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz#2d4260033e199b3032a08b41348ac10de21c47e9" + integrity sha512-RbhOOTCNoCrbfkRyoXODZp75MlpiHMgbE5MEBZAnnnLyQNgrigEj4p0lzsMDyc1zVsJDLrivB58tgg3emX0eEA== "@scena/dragscroll@^1.4.0": version "1.4.0" @@ -2799,7 +2722,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@18.2.42", "@types/react@^18.2.42": +"@types/react@*", "@types/react@^18.2.42": version "18.2.42" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7" integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA== @@ -2883,16 +2806,16 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/eslint-plugin@^6.13.2": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.16.0.tgz#cc29fbd208ea976de3db7feb07755bba0ce8d8bc" - integrity sha512-O5f7Kv5o4dLWQtPX4ywPPa+v9G+1q1x8mz0Kr0pXUtKsevo+gIJHLkGc8RxaZWtP8RrhwhSNIWThnW42K9/0rQ== +"@typescript-eslint/eslint-plugin@^7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.1.tgz#dd71fc5c7ecec745ca26ece506d84d203a205c0e" + integrity sha512-zioDz623d0RHNhvx0eesUmGfIjzrk18nSBC8xewepKXbBvN/7c1qImV7Hg8TI1URTxKax7/zxfxj3Uph8Chcuw== dependencies: "@eslint-community/regexpp" "^4.5.1" - "@typescript-eslint/scope-manager" "6.16.0" - "@typescript-eslint/type-utils" "6.16.0" - "@typescript-eslint/utils" "6.16.0" - "@typescript-eslint/visitor-keys" "6.16.0" + "@typescript-eslint/scope-manager" "7.1.1" + "@typescript-eslint/type-utils" "7.1.1" + "@typescript-eslint/utils" "7.1.1" + "@typescript-eslint/visitor-keys" "7.1.1" debug "^4.3.4" graphemer "^1.4.0" ignore "^5.2.4" @@ -2900,14 +2823,26 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/parser@^5.21.0", "@typescript-eslint/parser@^5.42.0", "@typescript-eslint/parser@^5.48.2": - version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.62.0.tgz#1b63d082d849a2fcae8a569248fbe2ee1b8a56c7" - integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA== +"@typescript-eslint/parser@^5.4.2 || ^6.0.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" + integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== dependencies: - "@typescript-eslint/scope-manager" "5.62.0" - "@typescript-eslint/types" "5.62.0" - "@typescript-eslint/typescript-estree" "5.62.0" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/typescript-estree" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + debug "^4.3.4" + +"@typescript-eslint/parser@^7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.1.1.tgz#6a9d0a5c9ccdf5dbd3cb8c949728c64e24e07d1f" + integrity sha512-ZWUFyL0z04R1nAEgr9e79YtV5LbafdOtN7yapNbn1ansMyaegl2D4bL7vHoJ4HPSc4CaLwuCVas8CVuneKzplQ== + dependencies: + "@typescript-eslint/scope-manager" "7.1.1" + "@typescript-eslint/types" "7.1.1" + "@typescript-eslint/typescript-estree" "7.1.1" + "@typescript-eslint/visitor-keys" "7.1.1" debug "^4.3.4" "@typescript-eslint/scope-manager@5.62.0": @@ -2918,13 +2853,21 @@ "@typescript-eslint/types" "5.62.0" "@typescript-eslint/visitor-keys" "5.62.0" -"@typescript-eslint/scope-manager@6.16.0": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.16.0.tgz#f3e9a00fbc1d0701356359cd56489c54d9e37168" - integrity sha512-0N7Y9DSPdaBQ3sqSCwlrm9zJwkpOuc6HYm7LpzLAPqBL7dmzAUimr4M29dMkOP/tEwvOCC/Cxo//yOfJD3HUiw== +"@typescript-eslint/scope-manager@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" + integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== dependencies: - "@typescript-eslint/types" "6.16.0" - "@typescript-eslint/visitor-keys" "6.16.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + +"@typescript-eslint/scope-manager@7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.1.1.tgz#9e301803ff8e21a74f50c6f89a4baccad9a48f93" + integrity sha512-cirZpA8bJMRb4WZ+rO6+mnOJrGFDd38WoXCEI57+CYBqta8Yc8aJym2i7vyqLL1vVYljgw0X27axkUXz32T8TA== + dependencies: + "@typescript-eslint/types" "7.1.1" + "@typescript-eslint/visitor-keys" "7.1.1" "@typescript-eslint/type-utils@5.62.0": version "5.62.0" @@ -2936,13 +2879,13 @@ debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/type-utils@6.16.0": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.16.0.tgz#5f21c3e49e540ad132dc87fc99af463c184d5ed1" - integrity sha512-ThmrEOcARmOnoyQfYkHw/DX2SEYBalVECmoldVuH6qagKROp/jMnfXpAU/pAIWub9c4YTxga+XwgAkoA0pxfmg== +"@typescript-eslint/type-utils@7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.1.1.tgz#aee820d5bedd39b83c18585a526cc520ddb7a226" + integrity sha512-5r4RKze6XHEEhlZnJtR3GYeCh1IueUHdbrukV2KSlLXaTjuSfeVF8mZUVPLovidCuZfbVjfhi4c0DNSa/Rdg5g== dependencies: - "@typescript-eslint/typescript-estree" "6.16.0" - "@typescript-eslint/utils" "6.16.0" + "@typescript-eslint/typescript-estree" "7.1.1" + "@typescript-eslint/utils" "7.1.1" debug "^4.3.4" ts-api-utils "^1.0.1" @@ -2951,10 +2894,15 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== -"@typescript-eslint/types@6.16.0": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.16.0.tgz#a3abe0045737d44d8234708d5ed8fef5d59dc91e" - integrity sha512-hvDFpLEvTJoHutVl87+MG/c5C8I6LOgEx05zExTSJDEVU7hhR3jhV8M5zuggbdFCw98+HhZWPHZeKS97kS3JoQ== +"@typescript-eslint/types@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" + integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== + +"@typescript-eslint/types@7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.1.1.tgz#ca33ba7cf58224fb46a84fea62593c2c53cd795f" + integrity sha512-KhewzrlRMrgeKm1U9bh2z5aoL4s7K3tK5DwHDn8MHv0yQfWFz/0ZR6trrIHHa5CsF83j/GgHqzdbzCXJ3crx0Q== "@typescript-eslint/typescript-estree@5.62.0": version "5.62.0" @@ -2969,13 +2917,27 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@6.16.0": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.16.0.tgz#d6e0578e4f593045f0df06c4b3a22bd6f13f2d03" - integrity sha512-VTWZuixh/vr7nih6CfrdpmFNLEnoVBF1skfjdyGnNwXOH1SLeHItGdZDHhhAIzd3ACazyY2Fg76zuzOVTaknGA== +"@typescript-eslint/typescript-estree@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" + integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== dependencies: - "@typescript-eslint/types" "6.16.0" - "@typescript-eslint/visitor-keys" "6.16.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + minimatch "9.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/typescript-estree@7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.1.tgz#09c54af0151a1b05d0875c0fc7fe2ec7a2476ece" + integrity sha512-9ZOncVSfr+sMXVxxca2OJOPagRwT0u/UHikM2Rd6L/aB+kL/QAuTnsv6MeXtjzCJYb8PzrXarypSGIPx3Jemxw== + dependencies: + "@typescript-eslint/types" "7.1.1" + "@typescript-eslint/visitor-keys" "7.1.1" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" @@ -2997,17 +2959,17 @@ eslint-scope "^5.1.1" semver "^7.3.7" -"@typescript-eslint/utils@6.16.0": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.16.0.tgz#1c291492d34670f9210d2b7fcf6b402bea3134ae" - integrity sha512-T83QPKrBm6n//q9mv7oiSvy/Xq/7Hyw9SzSEhMHJwznEmQayfBM87+oAlkNAMEO7/MjIwKyOHgBJbxB0s7gx2A== +"@typescript-eslint/utils@7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.1.1.tgz#bdeeb789eee4af5d3fb5400a69566d4dbf97ff3b" + integrity sha512-thOXM89xA03xAE0lW7alstvnyoBUbBX38YtY+zAUcpRPcq9EIhXPuJ0YTv948MbzmKh6e1AUszn5cBFK49Umqg== dependencies: "@eslint-community/eslint-utils" "^4.4.0" "@types/json-schema" "^7.0.12" "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "6.16.0" - "@typescript-eslint/types" "6.16.0" - "@typescript-eslint/typescript-estree" "6.16.0" + "@typescript-eslint/scope-manager" "7.1.1" + "@typescript-eslint/types" "7.1.1" + "@typescript-eslint/typescript-estree" "7.1.1" semver "^7.5.4" "@typescript-eslint/visitor-keys@5.62.0": @@ -3018,12 +2980,20 @@ "@typescript-eslint/types" "5.62.0" eslint-visitor-keys "^3.3.0" -"@typescript-eslint/visitor-keys@6.16.0": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.16.0.tgz#d50da18a05d91318ed3e7e8889bda0edc35f3a10" - integrity sha512-QSFQLruk7fhs91a/Ep/LqRdbJCZ1Rq03rqBdKT5Ky17Sz8zRLUksqIe9DW0pKtg/Z35/ztbLQ6qpOCN6rOC11A== +"@typescript-eslint/visitor-keys@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" + integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== dependencies: - "@typescript-eslint/types" "6.16.0" + "@typescript-eslint/types" "6.21.0" + eslint-visitor-keys "^3.4.1" + +"@typescript-eslint/visitor-keys@7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.1.tgz#e6538a58c9b157f03bcbb29e3b6a92fe39a6ab0d" + integrity sha512-yTdHDQxY7cSoCcAtiBzVzxleJhkGB9NncSIyMYe2+OGON1ZsP9zOPws/Pqgopa65jvknOjlk/w7ulPlZ78PiLQ== + dependencies: + "@typescript-eslint/types" "7.1.1" eslint-visitor-keys "^3.4.1" "@ungap/structured-clone@^1.2.0": @@ -3031,16 +3001,11 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -acorn-jsx@^5.3.1, acorn-jsx@^5.3.2: +acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^7.4.0: - version "7.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" - integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== - acorn@^8.8.2, acorn@^8.9.0: version "8.11.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" @@ -3058,7 +3023,7 @@ ajv-keywords@^3.5.2: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: +ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -3068,7 +3033,7 @@ ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.1, ajv@^8.6.0: +ajv@^8.6.0: version "8.12.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== @@ -3078,11 +3043,6 @@ ajv@^8.0.1, ajv@^8.6.0: require-from-string "^2.0.2" uri-js "^4.2.2" -ansi-colors@^4.1.1: - version "4.1.3" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" - integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== - ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -3130,13 +3090,6 @@ arg@^5.0.2: resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" @@ -3164,7 +3117,7 @@ array-buffer-byte-length@^1.0.0: call-bind "^1.0.2" is-array-buffer "^3.0.1" -array-includes@^3.1.5, array-includes@^3.1.6, array-includes@^3.1.7: +array-includes@^3.1.6, array-includes@^3.1.7: version "3.1.7" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.7.tgz#8cd2e01b26f7a3086cbc87271593fe921c62abda" integrity sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ== @@ -3213,7 +3166,7 @@ array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: es-abstract "^1.22.1" es-shim-unscopables "^1.0.0" -array.prototype.flatmap@^1.3.0, array.prototype.flatmap@^1.3.1, array.prototype.flatmap@^1.3.2: +array.prototype.flatmap@^1.3.1, array.prototype.flatmap@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== @@ -3252,11 +3205,6 @@ ast-types-flow@^0.0.8: resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6" integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== -astral-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" - integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== - async@^3.2.3: version "3.2.5" resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" @@ -3909,7 +3857,7 @@ date-fns@^2.30.0: dependencies: "@babel/runtime" "^7.21.0" -debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: +debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -4146,14 +4094,6 @@ enhanced-resolve@^5.12.0: graceful-fs "^4.2.4" tapable "^2.2.0" -enquirer@^2.3.5: - version "2.4.1" - resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.4.1.tgz#93334b3fbd74fc7097b224ab4a8fb7e40bf4ae56" - integrity sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ== - dependencies: - ansi-colors "^4.1.1" - strip-ansi "^6.0.1" - entities@^4.4.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" @@ -4432,77 +4372,32 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-config-next@12.2.2: - version "12.2.2" - resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-12.2.2.tgz#4bb996026e118071849bc4011283a160ad5bde46" - integrity sha512-oJhWBLC4wDYYUFv/5APbjHUFd0QRFCojMdj/QnMoOEktmeTvwnnoA8F8uaXs0fQgsaTK0tbUxBRv9/Y4/rpxOA== +eslint-config-next@^14.1.0: + version "14.1.0" + resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-14.1.0.tgz#7e309d426b8afacaba3b32fdbb02ba220b6d0a97" + integrity sha512-SBX2ed7DoRFXC6CQSLc/SbLY9Ut6HxNB2wPTcoIWjUMd7aF7O/SIE7111L8FdZ9TXsNV4pulUDnfthpyPtbFUg== dependencies: - "@next/eslint-plugin-next" "12.2.2" - "@rushstack/eslint-patch" "^1.1.3" - "@typescript-eslint/parser" "^5.21.0" - eslint-import-resolver-node "^0.3.6" - eslint-import-resolver-typescript "^2.7.1" - eslint-plugin-import "^2.26.0" - eslint-plugin-jsx-a11y "^6.5.1" - eslint-plugin-react "^7.29.4" - eslint-plugin-react-hooks "^4.5.0" - -eslint-config-next@13.0.0: - version "13.0.0" - resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-13.0.0.tgz#d533ee1dbd6576fd3759ba4db4d5a6c4e039c242" - integrity sha512-y2nqWS2tycWySdVhb+rhp6CuDmDazGySqkzzQZf3UTyfHyC7og1m5m/AtMFwCo5mtvDqvw1BENin52kV9733lg== - dependencies: - "@next/eslint-plugin-next" "13.0.0" - "@rushstack/eslint-patch" "^1.1.3" - "@typescript-eslint/parser" "^5.21.0" - eslint-import-resolver-node "^0.3.6" - eslint-import-resolver-typescript "^2.7.1" - eslint-plugin-import "^2.26.0" - eslint-plugin-jsx-a11y "^6.5.1" - eslint-plugin-react "^7.31.7" - eslint-plugin-react-hooks "^4.5.0" - -eslint-config-next@13.2.1: - version "13.2.1" - resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-13.2.1.tgz#644fb3496b832bc1e32f2c57cce1ec3eeb7bb7a1" - integrity sha512-2GAx7EjSiCzJN6H2L/v1kbYrNiwQxzkyjy6eWSjuhAKt+P6d3nVNHGy9mON8ZcYd72w/M8kyMjm4UB9cvijgrw== - dependencies: - "@next/eslint-plugin-next" "13.2.1" - "@rushstack/eslint-patch" "^1.1.3" - "@typescript-eslint/parser" "^5.42.0" + "@next/eslint-plugin-next" "14.1.0" + "@rushstack/eslint-patch" "^1.3.3" + "@typescript-eslint/parser" "^5.4.2 || ^6.0.0" eslint-import-resolver-node "^0.3.6" eslint-import-resolver-typescript "^3.5.2" - eslint-plugin-import "^2.26.0" - eslint-plugin-jsx-a11y "^6.5.1" - eslint-plugin-react "^7.31.7" - eslint-plugin-react-hooks "^4.5.0" + eslint-plugin-import "^2.28.1" + eslint-plugin-jsx-a11y "^6.7.1" + eslint-plugin-react "^7.33.2" + eslint-plugin-react-hooks "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" -eslint-config-next@13.2.4: - version "13.2.4" - resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-13.2.4.tgz#8aa4d42da3a575a814634ba9c88c8d25266c5fdd" - integrity sha512-lunIBhsoeqw6/Lfkd6zPt25w1bn0znLA/JCL+au1HoEpSb4/PpsOYsYtgV/q+YPsoKIOzFyU5xnb04iZnXjUvg== +eslint-config-prettier@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz#31af3d94578645966c082fcb71a5846d3c94867f" + integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw== + +eslint-config-turbo@^1.12.4: + version "1.12.4" + resolved "https://registry.yarnpkg.com/eslint-config-turbo/-/eslint-config-turbo-1.12.4.tgz#b911aced2228e98176dbebe0f1ebef345a253400" + integrity sha512-5hqEaV6PNmAYLL4RTmq74OcCt8pgzOLnfDVPG/7PUXpQ0Mpz0gr926oCSFukywKKXjdum3VHD84S7Z9A/DqTAw== dependencies: - "@next/eslint-plugin-next" "13.2.4" - "@rushstack/eslint-patch" "^1.1.3" - "@typescript-eslint/parser" "^5.42.0" - eslint-import-resolver-node "^0.3.6" - eslint-import-resolver-typescript "^3.5.2" - eslint-plugin-import "^2.26.0" - eslint-plugin-jsx-a11y "^6.5.1" - eslint-plugin-react "^7.31.7" - eslint-plugin-react-hooks "^4.5.0" - -eslint-config-prettier@^8.3.0: - version "8.10.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz#3a06a662130807e2502fc3ff8b4143d8a0658e11" - integrity sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg== - -eslint-config-turbo@latest: - version "1.11.2" - resolved "https://registry.yarnpkg.com/eslint-config-turbo/-/eslint-config-turbo-1.11.2.tgz#8e6c456f58e88ecc9adface9c5e03fa782e8bba5" - integrity sha512-vqbyCH6kCHFoIAWUmGL61c0BfUQNz0XAl2RzAnEkSQ+PLXvEvuV2HsvL51UOzyyElfJlzZuh9T4BvUqb5KR9Eg== - dependencies: - eslint-plugin-turbo "1.11.2" + eslint-plugin-turbo "1.12.4" eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9: version "0.3.9" @@ -4513,17 +4408,6 @@ eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9: is-core-module "^2.13.0" resolve "^1.22.4" -eslint-import-resolver-typescript@^2.7.1: - version "2.7.1" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.7.1.tgz#a90a4a1c80da8d632df25994c4c5fdcdd02b8751" - integrity sha512-00UbgGwV8bSgUv34igBDbTOtKhqoRMy9bFjNehT40bXg6585PNIct8HhXZ0SybqB9rWtXj9crcku8ndDn/gIqQ== - dependencies: - debug "^4.3.4" - glob "^7.2.0" - is-glob "^4.0.3" - resolve "^1.22.0" - tsconfig-paths "^3.14.1" - eslint-import-resolver-typescript@^3.5.2: version "3.6.1" resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz#7b983680edd3f1c5bce1a5829ae0bc2d57fe9efa" @@ -4544,7 +4428,7 @@ eslint-module-utils@^2.7.4, eslint-module-utils@^2.8.0: dependencies: debug "^3.2.7" -eslint-plugin-import@^2.26.0: +eslint-plugin-import@^2.28.1, eslint-plugin-import@^2.29.1: version "2.29.1" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz#d45b37b5ef5901d639c15270d74d46d161150643" integrity sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw== @@ -4567,7 +4451,7 @@ eslint-plugin-import@^2.26.0: semver "^6.3.1" tsconfig-paths "^3.15.0" -eslint-plugin-jsx-a11y@^6.5.1: +eslint-plugin-jsx-a11y@^6.7.1: version "6.8.0" resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz#2fa9c701d44fcd722b7c771ec322432857fcbad2" integrity sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA== @@ -4589,32 +4473,12 @@ eslint-plugin-jsx-a11y@^6.5.1: object.entries "^1.1.7" object.fromentries "^2.0.7" -eslint-plugin-react-hooks@^4.5.0: +"eslint-plugin-react-hooks@^4.5.0 || 5.0.0-canary-7118f5dd7-20230705": version "4.6.0" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3" integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== -eslint-plugin-react@7.31.8: - version "7.31.8" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.31.8.tgz#3a4f80c10be1bcbc8197be9e8b641b2a3ef219bf" - integrity sha512-5lBTZmgQmARLLSYiwI71tiGVTLUuqXantZM6vlSY39OaDSV0M7+32K5DnLkmFrwTe+Ksz0ffuLUC91RUviVZfw== - dependencies: - array-includes "^3.1.5" - array.prototype.flatmap "^1.3.0" - doctrine "^2.1.0" - estraverse "^5.3.0" - jsx-ast-utils "^2.4.1 || ^3.0.0" - minimatch "^3.1.2" - object.entries "^1.1.5" - object.fromentries "^2.0.5" - object.hasown "^1.1.1" - object.values "^1.1.5" - prop-types "^15.8.1" - resolve "^2.0.0-next.3" - semver "^6.3.0" - string.prototype.matchall "^4.0.7" - -eslint-plugin-react@^7.29.4, eslint-plugin-react@^7.31.7: +eslint-plugin-react@^7.33.2: version "7.33.2" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz#69ee09443ffc583927eafe86ffebb470ee737608" integrity sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw== @@ -4636,10 +4500,10 @@ eslint-plugin-react@^7.29.4, eslint-plugin-react@^7.31.7: semver "^6.3.1" string.prototype.matchall "^4.0.8" -eslint-plugin-turbo@1.11.2: - version "1.11.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-turbo/-/eslint-plugin-turbo-1.11.2.tgz#7bb450cced51d35369a678114c2ee9882937adc5" - integrity sha512-U6DX+WvgGFiwEAqtOjm4Ejd9O4jsw8jlFNkQi0ywxbMnbiTie+exF4Z0F/B1ajtjjeZkBkgRnlU+UkoraBN+bw== +eslint-plugin-turbo@1.12.4: + version "1.12.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-turbo/-/eslint-plugin-turbo-1.12.4.tgz#f29ddd89cb853db5dd4332db39ec2d85c713041e" + integrity sha512-3AGmXvH7E4i/XTWqBrcgu+G7YKZJV/8FrEn79kTd50ilNsv+U3nS2IlcCrQB6Xm2m9avGD9cadLzKDR1/UF2+g== dependencies: dotenv "16.0.3" @@ -4651,7 +4515,7 @@ eslint-scope@^5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-scope@^7.1.1, eslint-scope@^7.2.2: +eslint-scope@^7.2.2: version "7.2.2" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== @@ -4659,182 +4523,21 @@ eslint-scope@^7.1.1, eslint-scope@^7.2.2: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" - integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== - dependencies: - eslint-visitor-keys "^1.1.0" - -eslint-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" - integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== - dependencies: - eslint-visitor-keys "^2.0.0" - -eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" - integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== - -eslint-visitor-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" - integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== - eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint@8.34.0: - version "8.34.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.34.0.tgz#fe0ab0ef478104c1f9ebc5537e303d25a8fb22d6" - integrity sha512-1Z8iFsucw+7kSqXNZVslXS8Ioa4u2KM7GPwuKtkTFAqZ/cHMcEaR+1+Br0wLlot49cNxIiZk5wp8EAbPcYZxTg== - dependencies: - "@eslint/eslintrc" "^1.4.1" - "@humanwhocodes/config-array" "^0.11.8" - "@humanwhocodes/module-importer" "^1.0.1" - "@nodelib/fs.walk" "^1.2.8" - ajv "^6.10.0" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.3.2" - doctrine "^3.0.0" - escape-string-regexp "^4.0.0" - eslint-scope "^7.1.1" - eslint-utils "^3.0.0" - eslint-visitor-keys "^3.3.0" - espree "^9.4.0" - esquery "^1.4.0" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" - find-up "^5.0.0" - glob-parent "^6.0.2" - globals "^13.19.0" - grapheme-splitter "^1.0.4" - ignore "^5.2.0" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - is-path-inside "^3.0.3" - js-sdsl "^4.1.4" - js-yaml "^4.1.0" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.1.2" - natural-compare "^1.4.0" - optionator "^0.9.1" - regexpp "^3.2.0" - strip-ansi "^6.0.1" - strip-json-comments "^3.1.0" - text-table "^0.2.0" - -eslint@8.36.0: - version "8.36.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.36.0.tgz#1bd72202200a5492f91803b113fb8a83b11285cf" - integrity sha512-Y956lmS7vDqomxlaaQAHVmeb4tNMp2FWIvU/RnU5BD3IKMD/MJPr76xdyr68P8tV1iNMvN2mRK0yy3c+UjL+bw== - dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - "@eslint-community/regexpp" "^4.4.0" - "@eslint/eslintrc" "^2.0.1" - "@eslint/js" "8.36.0" - "@humanwhocodes/config-array" "^0.11.8" - "@humanwhocodes/module-importer" "^1.0.1" - "@nodelib/fs.walk" "^1.2.8" - ajv "^6.10.0" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.3.2" - doctrine "^3.0.0" - escape-string-regexp "^4.0.0" - eslint-scope "^7.1.1" - eslint-visitor-keys "^3.3.0" - espree "^9.5.0" - esquery "^1.4.2" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" - find-up "^5.0.0" - glob-parent "^6.0.2" - globals "^13.19.0" - grapheme-splitter "^1.0.4" - ignore "^5.2.0" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - is-path-inside "^3.0.3" - js-sdsl "^4.1.4" - js-yaml "^4.1.0" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.1.2" - natural-compare "^1.4.0" - optionator "^0.9.1" - strip-ansi "^6.0.1" - strip-json-comments "^3.1.0" - text-table "^0.2.0" - -eslint@^7.23.0, eslint@^7.32.0: - version "7.32.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" - integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA== - dependencies: - "@babel/code-frame" "7.12.11" - "@eslint/eslintrc" "^0.4.3" - "@humanwhocodes/config-array" "^0.5.0" - ajv "^6.10.0" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.0.1" - doctrine "^3.0.0" - enquirer "^2.3.5" - escape-string-regexp "^4.0.0" - eslint-scope "^5.1.1" - eslint-utils "^2.1.0" - eslint-visitor-keys "^2.0.0" - espree "^7.3.1" - esquery "^1.4.0" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^5.1.2" - globals "^13.6.0" - ignore "^4.0.6" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - js-yaml "^3.13.1" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.0.4" - natural-compare "^1.4.0" - optionator "^0.9.1" - progress "^2.0.0" - regexpp "^3.1.0" - semver "^7.2.1" - strip-ansi "^6.0.0" - strip-json-comments "^3.1.0" - table "^6.0.9" - text-table "^0.2.0" - v8-compile-cache "^2.0.3" - -eslint@^8.31.0: - version "8.56.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.56.0.tgz#4957ce8da409dc0809f99ab07a1b94832ab74b15" - integrity sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ== +eslint@^8.57.0: + version "8.57.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" + integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.6.1" "@eslint/eslintrc" "^2.1.4" - "@eslint/js" "8.56.0" - "@humanwhocodes/config-array" "^0.11.13" + "@eslint/js" "8.57.0" + "@humanwhocodes/config-array" "^0.11.14" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" "@ungap/structured-clone" "^1.2.0" @@ -4869,16 +4572,7 @@ eslint@^8.31.0: strip-ansi "^6.0.1" text-table "^0.2.0" -espree@^7.3.0, espree@^7.3.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" - integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== - dependencies: - acorn "^7.4.0" - acorn-jsx "^5.3.1" - eslint-visitor-keys "^1.3.0" - -espree@^9.4.0, espree@^9.5.0, espree@^9.6.0, espree@^9.6.1: +espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== @@ -4887,12 +4581,7 @@ espree@^9.4.0, espree@^9.5.0, espree@^9.6.0, espree@^9.6.1: acorn-jsx "^5.3.2" eslint-visitor-keys "^3.4.1" -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -esquery@^1.4.0, esquery@^1.4.2: +esquery@^1.4.2: version "1.5.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== @@ -5162,11 +4851,6 @@ function.prototype.name@^1.1.5, function.prototype.name@^1.1.6: es-abstract "^1.22.1" functions-have-names "^1.2.3" -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== - functions-have-names@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" @@ -5249,19 +4933,7 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@7.1.7: - version "7.1.7" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^10.3.10: +glob@10.3.10, glob@^10.3.10: version "10.3.10" resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b" integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== @@ -5272,7 +4944,7 @@ glob@^10.3.10: minipass "^5.0.0 || ^6.0.2 || ^7.0.0" path-scurry "^1.10.1" -glob@^7.0.3, glob@^7.1.3, glob@^7.1.6, glob@^7.2.0: +glob@^7.0.3, glob@^7.1.3, glob@^7.1.6: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -5300,7 +4972,7 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^13.19.0, globals@^13.6.0, globals@^13.9.0: +globals@^13.19.0: version "13.24.0" resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== @@ -5349,11 +5021,6 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== -grapheme-splitter@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" - integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== - graphemer@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" @@ -5463,11 +5130,6 @@ ieee754@^1.1.13: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - ignore@^5.2.0, ignore@^5.2.4: version "5.3.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" @@ -5478,7 +5140,7 @@ immediate@~3.0.5: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== -import-fresh@^3.0.0, import-fresh@^3.2.1: +import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -5877,24 +5539,11 @@ js-cookie@^3.0.1: resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc" integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw== -js-sdsl@^4.1.4: - version "4.4.2" - resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.2.tgz#2e3c031b1f47d3aca8b775532e3ebb0818e7f847" - integrity sha512-dwXFwByc/ajSV6m5bcKAPwe4yDDF6D614pxmIi5odytzxRlwqF6nwoiCek80Ixc7Cvma5awClxrzFtxCQvcM8w== - "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.13.1: - version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" @@ -6143,11 +5792,6 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== -lodash.truncate@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" - integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== - lodash@^4.0.1, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -6577,7 +6221,7 @@ minimatch@9.0.3, minimatch@^9.0.1: dependencies: brace-expansion "^2.0.1" -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -6819,7 +6463,7 @@ object.assign@^4.1.4: has-symbols "^1.0.3" object-keys "^1.1.1" -object.entries@^1.1.5, object.entries@^1.1.6, object.entries@^1.1.7: +object.entries@^1.1.6, object.entries@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.7.tgz#2b47760e2a2e3a752f39dd874655c61a7f03c131" integrity sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA== @@ -6828,7 +6472,7 @@ object.entries@^1.1.5, object.entries@^1.1.6, object.entries@^1.1.7: define-properties "^1.2.0" es-abstract "^1.22.1" -object.fromentries@^2.0.5, object.fromentries@^2.0.6, object.fromentries@^2.0.7: +object.fromentries@^2.0.6, object.fromentries@^2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.7.tgz#71e95f441e9a0ea6baf682ecaaf37fa2a8d7e616" integrity sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA== @@ -6847,7 +6491,7 @@ object.groupby@^1.0.1: es-abstract "^1.22.1" get-intrinsic "^1.2.1" -object.hasown@^1.1.1, object.hasown@^1.1.2: +object.hasown@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.3.tgz#6a5f2897bb4d3668b8e79364f98ccf971bda55ae" integrity sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA== @@ -6869,7 +6513,7 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -object.values@^1.1.5, object.values@^1.1.6, object.values@^1.1.7: +object.values@^1.1.6, object.values@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.7.tgz#617ed13272e7e1071b43973aa1655d9291b8442a" integrity sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng== @@ -6892,7 +6536,7 @@ onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" -optionator@^0.9.1, optionator@^0.9.3: +optionator@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== @@ -7221,7 +6865,7 @@ pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== -progress@^2.0.0, progress@^2.0.3: +progress@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== @@ -7724,11 +7368,6 @@ regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1: define-properties "^1.2.0" set-function-name "^2.0.0" -regexpp@^3.1.0, regexpp@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" - integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== - regexpu-core@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" @@ -7787,7 +7426,7 @@ resolve-pkg-maps@^1.0.0: resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== -resolve@1.22.8, resolve@^1.1.7, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22.0, resolve@^1.22.2, resolve@^1.22.4: +resolve@1.22.8, resolve@^1.1.7, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22.2, resolve@^1.22.4: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -7796,7 +7435,7 @@ resolve@1.22.8, resolve@^1.1.7, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22. path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@^2.0.0-next.3, resolve@^2.0.0-next.4: +resolve@^2.0.0-next.4: version "2.0.0-next.5" resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== @@ -7952,12 +7591,12 @@ selecto@~1.26.3: keycon "^1.2.0" overlap-area "^1.1.0" -semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: +semver@^6.0.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.2.1, semver@^7.3.5, semver@^7.3.7, semver@^7.5.4: +semver@^7.3.5, semver@^7.3.7, semver@^7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -8077,15 +7716,6 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -slice-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" - integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - snake-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" @@ -8144,11 +7774,6 @@ space-separated-tokens@^2.0.0: resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== - stacktrace-parser@^0.1.10: version "0.1.10" resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz#29fb0cae4e0d0b85155879402857a1639eb6051a" @@ -8169,7 +7794,8 @@ streamx@^2.15.0: fast-fifo "^1.1.0" queue-tick "^1.0.1" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: + name string-width-cjs version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -8187,7 +7813,7 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" -string.prototype.matchall@^4.0.6, string.prototype.matchall@^4.0.7, string.prototype.matchall@^4.0.8: +string.prototype.matchall@^4.0.6, string.prototype.matchall@^4.0.8: version "4.0.10" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz#a1553eb532221d4180c51581d6072cd65d1ee100" integrity sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ== @@ -8246,6 +7872,7 @@ stringify-object@^3.3.0: is-regexp "^1.0.0" "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: + name strip-ansi-cjs version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -8274,7 +7901,7 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: +strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -8355,17 +7982,6 @@ tabbable@^6.0.1: resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97" integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew== -table@^6.0.9: - version "6.8.1" - resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf" - integrity sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA== - dependencies: - ajv "^8.0.1" - lodash.truncate "^4.4.2" - slice-ansi "^4.0.0" - string-width "^4.2.3" - strip-ansi "^6.0.1" - tailwind-merge@^1.14.0: version "1.14.0" resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-1.14.0.tgz#e677f55d864edc6794562c63f5001f45093cdb8b" @@ -8591,7 +8207,7 @@ ts-interface-checker@^0.1.9: resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== -tsconfig-paths@^3.14.1, tsconfig-paths@^3.15.0: +tsconfig-paths@^3.15.0: version "3.15.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== @@ -8788,11 +8404,16 @@ typescript@4.7.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== -typescript@4.9.5, typescript@^4.7.4: +typescript@4.9.5: version "4.9.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +typescript@^5.3.3: + version "5.3.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" + integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== + uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" @@ -8993,11 +8614,6 @@ uvu@^0.5.0: kleur "^4.0.3" sade "^1.7.3" -v8-compile-cache@^2.0.3: - version "2.4.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz#cdada8bec61e15865f05d097c5f4fd30e94dc128" - integrity sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw== - vfile-message@^3.0.0: version "3.1.4" resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.4.tgz#15a50816ae7d7c2d1fa87090a7f9f96612b59dea" From cace132a2a670fef0c3acbc5543475587b5ff007 Mon Sep 17 00:00:00 2001 From: "M. Palanikannan" <73993394+Palanikannan1437@users.noreply.github.com> Date: Wed, 6 Mar 2024 18:43:19 +0530 Subject: [PATCH 043/308] [WEB-372] fix: horizontal rule extension now always divider adds below nodes (#3890) * fix: horizontal rule extension now always divider adds below nodes * chore: removing duplicate horizontal rule extension --- .../horizontal-rule/horizontal-rule.ts | 111 ++++++++++++++++++ .../editor/core/src/ui/extensions/index.tsx | 9 +- 2 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 packages/editor/core/src/ui/extensions/horizontal-rule/horizontal-rule.ts diff --git a/packages/editor/core/src/ui/extensions/horizontal-rule/horizontal-rule.ts b/packages/editor/core/src/ui/extensions/horizontal-rule/horizontal-rule.ts new file mode 100644 index 000000000..2af845b7a --- /dev/null +++ b/packages/editor/core/src/ui/extensions/horizontal-rule/horizontal-rule.ts @@ -0,0 +1,111 @@ +import { isNodeSelection, mergeAttributes, Node, nodeInputRule } from "@tiptap/core"; +import { NodeSelection, TextSelection } from "@tiptap/pm/state"; + +export interface HorizontalRuleOptions { + HTMLAttributes: Record; +} + +declare module "@tiptap/core" { + interface Commands { + horizontalRule: { + /** + * Add a horizontal rule + */ + setHorizontalRule: () => ReturnType; + }; + } +} + +export const CustomHorizontalRule = Node.create({ + name: "horizontalRule", + + addOptions() { + return { + HTMLAttributes: {}, + }; + }, + + group: "block", + + parseHTML() { + return [{ tag: "hr" }]; + }, + + renderHTML({ HTMLAttributes }) { + return ["hr", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]; + }, + + addCommands() { + return { + setHorizontalRule: + () => + ({ chain, state }) => { + const { selection } = state; + const { $from: $originFrom, $to: $originTo } = selection; + + const currentChain = chain(); + + if ($originFrom.parentOffset === 0) { + currentChain.insertContentAt( + { + from: Math.max($originFrom.pos - 1, 0), + to: $originTo.pos, + }, + { + type: this.name, + } + ); + } else if (isNodeSelection(selection)) { + currentChain.insertContentAt($originTo.pos, { + type: this.name, + }); + } else { + currentChain.insertContent({ type: this.name }); + } + + return ( + currentChain + // set cursor after horizontal rule + .command(({ tr, dispatch }) => { + if (dispatch) { + const { $to } = tr.selection; + const posAfter = $to.end(); + + if ($to.nodeAfter) { + if ($to.nodeAfter.isTextblock) { + tr.setSelection(TextSelection.create(tr.doc, $to.pos + 1)); + } else if ($to.nodeAfter.isBlock) { + tr.setSelection(NodeSelection.create(tr.doc, $to.pos)); + } else { + tr.setSelection(TextSelection.create(tr.doc, $to.pos)); + } + } else { + // add node after horizontal rule if it’s the end of the document + const node = $to.parent.type.contentMatch.defaultType?.create(); + + if (node) { + tr.insert(posAfter, node); + tr.setSelection(TextSelection.create(tr.doc, posAfter + 1)); + } + } + + tr.scrollIntoView(); + } + + return true; + }) + .run() + ); + }, + }; + }, + + addInputRules() { + return [ + nodeInputRule({ + find: /^(?:---|—-|___\s|\*\*\*\s)$/, + type: this.type, + }), + ]; + }, +}); diff --git a/packages/editor/core/src/ui/extensions/index.tsx b/packages/editor/core/src/ui/extensions/index.tsx index 190731fe0..7da381e98 100644 --- a/packages/editor/core/src/ui/extensions/index.tsx +++ b/packages/editor/core/src/ui/extensions/index.tsx @@ -27,6 +27,7 @@ import { RestoreImage } from "src/types/restore-image"; import { CustomLinkExtension } from "src/ui/extensions/custom-link"; import { CustomCodeInlineExtension } from "src/ui/extensions/code-inline"; import { CustomTypographyExtension } from "src/ui/extensions/typography"; +import { CustomHorizontalRule } from "./horizontal-rule/horizontal-rule"; export const CoreEditorExtensions = ( mentionConfig: { @@ -55,9 +56,7 @@ export const CoreEditorExtensions = ( }, code: false, codeBlock: false, - horizontalRule: { - HTMLAttributes: { class: "mt-4 mb-4" }, - }, + horizontalRule: false, blockquote: false, dropcursor: { color: "rgba(var(--color-text-100))", @@ -67,6 +66,10 @@ export const CoreEditorExtensions = ( CustomQuoteExtension.configure({ HTMLAttributes: { className: "border-l-4 border-custom-border-300" }, }), + + CustomHorizontalRule.configure({ + HTMLAttributes: { class: "mt-4 mb-4" }, + }), CustomKeymap, ListKeymap, CustomLinkExtension.configure({ From b3d3c0fb06c32421ba6378ffb944f9c9f21ae940 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 6 Mar 2024 18:46:36 +0530 Subject: [PATCH 044/308] [WEB-654] fix: enums export in the types package (#3887) * fix: enums export in the types package * chore: remove NestedKeyOf type --- .../dashboard.d.ts => dashboard.ts} | 18 +++++++++---- packages/types/src/dashboard/enums.ts | 8 ------ packages/types/src/dashboard/index.ts | 2 -- packages/types/src/index.d.ts | 10 -------- .../dashboard/widgets/assigned-issues.tsx | 10 ++++---- .../dashboard/widgets/created-issues.tsx | 10 ++++---- .../widgets/dropdowns/duration-filter.tsx | 3 +-- .../widgets/issue-panels/tabs-list.tsx | 4 +-- .../dashboard/widgets/issues-by-priority.tsx | 5 +++- .../widgets/issues-by-state-group.tsx | 25 ++++++++----------- web/constants/dashboard.ts | 11 +++++++- web/helpers/dashboard.helper.ts | 4 +-- 12 files changed, 52 insertions(+), 58 deletions(-) rename packages/types/src/{dashboard/dashboard.d.ts => dashboard.ts} (91%) delete mode 100644 packages/types/src/dashboard/enums.ts delete mode 100644 packages/types/src/dashboard/index.ts diff --git a/packages/types/src/dashboard/dashboard.d.ts b/packages/types/src/dashboard.ts similarity index 91% rename from packages/types/src/dashboard/dashboard.d.ts rename to packages/types/src/dashboard.ts index d565f6688..be7d7b3be 100644 --- a/packages/types/src/dashboard/dashboard.d.ts +++ b/packages/types/src/dashboard.ts @@ -1,8 +1,16 @@ -import { IIssueActivity, TIssuePriorities } from "../issues"; -import { TIssue } from "../issues/issue"; -import { TIssueRelationTypes } from "../issues/issue_relation"; -import { TStateGroups } from "../state"; -import { EDurationFilters } from "./enums"; +import { IIssueActivity, TIssuePriorities } from "./issues"; +import { TIssue } from "./issues/issue"; +import { TIssueRelationTypes } from "./issues/issue_relation"; +import { TStateGroups } from "./state"; + +enum EDurationFilters { + NONE = "none", + TODAY = "today", + THIS_WEEK = "this_week", + THIS_MONTH = "this_month", + THIS_YEAR = "this_year", + CUSTOM = "custom", +} export type TWidgetKeys = | "overview_stats" diff --git a/packages/types/src/dashboard/enums.ts b/packages/types/src/dashboard/enums.ts deleted file mode 100644 index 2c9efd5c3..000000000 --- a/packages/types/src/dashboard/enums.ts +++ /dev/null @@ -1,8 +0,0 @@ -export enum EDurationFilters { - NONE = "none", - TODAY = "today", - THIS_WEEK = "this_week", - THIS_MONTH = "this_month", - THIS_YEAR = "this_year", - CUSTOM = "custom", -} diff --git a/packages/types/src/dashboard/index.ts b/packages/types/src/dashboard/index.ts deleted file mode 100644 index dec14aea6..000000000 --- a/packages/types/src/dashboard/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./dashboard"; -export * from "./enums"; diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index b1eb38a56..bfebd92d0 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -29,13 +29,3 @@ export * from "./auth"; export * from "./api_token"; export * from "./instance"; export * from "./app"; - -export * from "./enums"; - -export type NestedKeyOf = { - [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object - ? ObjectType[Key] extends { pop: any; push: any } - ? `${Key}` - : `${Key}` | `${Key}.${NestedKeyOf}` - : `${Key}`; -}[keyof ObjectType & (string | number)]; diff --git a/web/components/dashboard/widgets/assigned-issues.tsx b/web/components/dashboard/widgets/assigned-issues.tsx index 3833d319c..1e031cacd 100644 --- a/web/components/dashboard/widgets/assigned-issues.tsx +++ b/web/components/dashboard/widgets/assigned-issues.tsx @@ -3,6 +3,8 @@ import { observer } from "mobx-react-lite"; import Link from "next/link"; import { Tab } from "@headlessui/react"; // hooks +import { useDashboard } from "hooks/store"; +// components import { DurationFilterDropdown, TabsList, @@ -10,14 +12,12 @@ import { WidgetLoader, WidgetProps, } from "components/dashboard/widgets"; -import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; -import { getCustomDates, getRedirectionFilters, getTabKey } from "helpers/dashboard.helper"; -import { useDashboard } from "hooks/store"; -// components // helpers +import { getCustomDates, getRedirectionFilters, getTabKey } from "helpers/dashboard.helper"; // types -import { EDurationFilters, TAssignedIssuesWidgetFilters, TAssignedIssuesWidgetResponse } from "@plane/types"; +import { TAssignedIssuesWidgetFilters, TAssignedIssuesWidgetResponse } from "@plane/types"; // constants +import { EDurationFilters, FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; const WIDGET_KEY = "assigned_issues"; diff --git a/web/components/dashboard/widgets/created-issues.tsx b/web/components/dashboard/widgets/created-issues.tsx index 61a1181e9..d36260f21 100644 --- a/web/components/dashboard/widgets/created-issues.tsx +++ b/web/components/dashboard/widgets/created-issues.tsx @@ -3,6 +3,8 @@ import { observer } from "mobx-react-lite"; import Link from "next/link"; import { Tab } from "@headlessui/react"; // hooks +import { useDashboard } from "hooks/store"; +// components import { DurationFilterDropdown, TabsList, @@ -10,14 +12,12 @@ import { WidgetLoader, WidgetProps, } from "components/dashboard/widgets"; -import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; -import { getCustomDates, getRedirectionFilters, getTabKey } from "helpers/dashboard.helper"; -import { useDashboard } from "hooks/store"; -// components // helpers +import { getCustomDates, getRedirectionFilters, getTabKey } from "helpers/dashboard.helper"; // types -import { EDurationFilters, TCreatedIssuesWidgetFilters, TCreatedIssuesWidgetResponse } from "@plane/types"; +import { TCreatedIssuesWidgetFilters, TCreatedIssuesWidgetResponse } from "@plane/types"; // constants +import { EDurationFilters, FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; const WIDGET_KEY = "created_issues"; diff --git a/web/components/dashboard/widgets/dropdowns/duration-filter.tsx b/web/components/dashboard/widgets/dropdowns/duration-filter.tsx index 3cf22c350..feef7ceca 100644 --- a/web/components/dashboard/widgets/dropdowns/duration-filter.tsx +++ b/web/components/dashboard/widgets/dropdowns/duration-filter.tsx @@ -6,9 +6,8 @@ import { DateFilterModal } from "components/core"; // ui // helpers import { getDurationFilterDropdownLabel } from "helpers/dashboard.helper"; -// types -import { EDurationFilters } from "@plane/types"; // constants +import { DURATION_FILTER_OPTIONS, EDurationFilters } from "constants/dashboard"; type Props = { customDates?: string[]; diff --git a/web/components/dashboard/widgets/issue-panels/tabs-list.tsx b/web/components/dashboard/widgets/issue-panels/tabs-list.tsx index d5fcea697..257f73851 100644 --- a/web/components/dashboard/widgets/issue-panels/tabs-list.tsx +++ b/web/components/dashboard/widgets/issue-panels/tabs-list.tsx @@ -1,11 +1,11 @@ import { observer } from "mobx-react"; import { Tab } from "@headlessui/react"; // helpers -import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; import { cn } from "helpers/common.helper"; // types -import { EDurationFilters, TIssuesListTypes } from "@plane/types"; +import { TIssuesListTypes } from "@plane/types"; // constants +import { EDurationFilters, FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; type Props = { durationFilter: EDurationFilters; diff --git a/web/components/dashboard/widgets/issues-by-priority.tsx b/web/components/dashboard/widgets/issues-by-priority.tsx index a8a8f64e8..becf32285 100644 --- a/web/components/dashboard/widgets/issues-by-priority.tsx +++ b/web/components/dashboard/widgets/issues-by-priority.tsx @@ -3,6 +3,7 @@ import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; // hooks +import { useDashboard } from "hooks/store"; // components import { DurationFilterDropdown, @@ -11,10 +12,12 @@ import { WidgetProps, } from "components/dashboard/widgets"; // helpers +import { getCustomDates } from "helpers/dashboard.helper"; // types +import { TIssuesByPriorityWidgetFilters, TIssuesByPriorityWidgetResponse } from "@plane/types"; // constants import { IssuesByPriorityGraph } from "components/graphs"; -import { EDurationFilters, TIssuesByPriorityWidgetFilters, TIssuesByPriorityWidgetResponse } from "@plane/types"; +import { EDurationFilters } from "constants/dashboard"; const WIDGET_KEY = "issues_by_priority"; diff --git a/web/components/dashboard/widgets/issues-by-state-group.tsx b/web/components/dashboard/widgets/issues-by-state-group.tsx index 6ffeda0c4..6857f7ef3 100644 --- a/web/components/dashboard/widgets/issues-by-state-group.tsx +++ b/web/components/dashboard/widgets/issues-by-state-group.tsx @@ -10,20 +10,15 @@ import { WidgetProps, } from "components/dashboard/widgets"; import { PieGraph } from "components/ui"; -import { STATE_GROUP_GRAPH_COLORS, STATE_GROUP_GRAPH_GRADIENTS } from "constants/dashboard"; import { STATE_GROUPS } from "constants/state"; import { getCustomDates } from "helpers/dashboard.helper"; import { useDashboard } from "hooks/store"; // components // helpers // types -import { - EDurationFilters, - TIssuesByStateGroupsWidgetFilters, - TIssuesByStateGroupsWidgetResponse, - TStateGroups, -} from "@plane/types"; +import { TIssuesByStateGroupsWidgetFilters, TIssuesByStateGroupsWidgetResponse, TStateGroups } from "@plane/types"; // constants +import { EDurationFilters, STATE_GROUP_GRAPH_COLORS, STATE_GROUP_GRAPH_GRADIENTS } from "constants/dashboard"; const WIDGET_KEY = "issues_by_state_groups"; @@ -84,14 +79,14 @@ export const IssuesByStateGroupWidget: React.FC = observer((props) startedCount > 0 ? "started" : unStartedCount > 0 - ? "unstarted" - : backlogCount > 0 - ? "backlog" - : completedCount > 0 - ? "completed" - : canceledCount > 0 - ? "cancelled" - : null; + ? "unstarted" + : backlogCount > 0 + ? "backlog" + : completedCount > 0 + ? "completed" + : canceledCount > 0 + ? "cancelled" + : null; setActiveStateGroup(stateGroup); setDefaultStateGroup(stateGroup); diff --git a/web/constants/dashboard.ts b/web/constants/dashboard.ts index a3f5f7e00..35599d661 100644 --- a/web/constants/dashboard.ts +++ b/web/constants/dashboard.ts @@ -10,7 +10,7 @@ import CompletedIssuesLight from "public/empty-state/dashboard/light/completed-i import OverdueIssuesLight from "public/empty-state/dashboard/light/overdue-issues.svg"; import UpcomingIssuesLight from "public/empty-state/dashboard/light/upcoming-issues.svg"; // types -import { EDurationFilters, TIssuesListTypes, TStateGroups } from "@plane/types"; +import { TIssuesListTypes, TStateGroups } from "@plane/types"; import { Props } from "components/icons/types"; // constants import { EUserWorkspaceRoles } from "./workspace"; @@ -117,6 +117,15 @@ export const STATE_GROUP_GRAPH_COLORS: Record = { cancelled: "#E5484D", }; +export enum EDurationFilters { + NONE = "none", + TODAY = "today", + THIS_WEEK = "this_week", + THIS_MONTH = "this_month", + THIS_YEAR = "this_year", + CUSTOM = "custom", +} + // filter duration options export const DURATION_FILTER_OPTIONS: { key: EDurationFilters; diff --git a/web/helpers/dashboard.helper.ts b/web/helpers/dashboard.helper.ts index a61ec7f78..c8c2e7746 100644 --- a/web/helpers/dashboard.helper.ts +++ b/web/helpers/dashboard.helper.ts @@ -2,9 +2,9 @@ import { endOfMonth, endOfWeek, endOfYear, startOfMonth, startOfWeek, startOfYea // helpers import { renderFormattedDate, renderFormattedPayloadDate } from "./date-time.helper"; // types -import { EDurationFilters, TIssuesListTypes } from "@plane/types"; +import { TIssuesListTypes } from "@plane/types"; // constants -import { DURATION_FILTER_OPTIONS } from "constants/dashboard"; +import { DURATION_FILTER_OPTIONS, EDurationFilters } from "constants/dashboard"; /** * @description returns date range based on the duration filter From e4f48d687830621cdebbbbbe83abaa93a14cdc25 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 6 Mar 2024 19:15:48 +0530 Subject: [PATCH 045/308] [WEB-393] feat: new emoji picker using `emoji-picker-react` (#3868) * chore: emoji-picker-react package added * chore: emoji and emoji picker component added * chore: emoji picker custom style added * chore: migration of the emoji's * chore: migration changes * chore: project logo prop * chore: added logo props in the serializer * chore: removed unused keys * chore: implement emoji picker throughout the web app * style: emoji icon picker * chore: update project logo renderer in the space app * chore: migrations fixes --------- Co-authored-by: Anmol Singh Bhatia Co-authored-by: NarayanBavisetti --- apiserver/plane/app/serializers/project.py | 3 +- apiserver/plane/app/views/workspace.py | 4 - .../db/migrations/0061_alter_issuelink_url.py | 18 - .../db/migrations/0061_project_logo_props.py | 54 + apiserver/plane/db/models/project.py | 1 + packages/types/src/projects.d.ts | 27 +- packages/types/src/users.d.ts | 4 - packages/ui/package.json | 1 + packages/ui/src/emoji/emoji-icon-picker.tsx | 169 +++ packages/ui/src/emoji/icons-list.tsx | 110 ++ packages/ui/src/emoji/icons.ts | 605 +++++++++ packages/ui/src/emoji/index.ts | 1 + packages/ui/src/form-fields/input.tsx | 27 +- packages/ui/src/index.ts | 1 + space/components/common/index.ts | 1 + space/components/common/project-logo.tsx | 34 + space/components/issues/navbar/index.tsx | 47 +- space/types/project.ts | 6 +- .../sidebar/projects-list.tsx | 14 +- .../sidebar/sidebar-header.tsx | 15 +- .../dashboard/widgets/recent-projects.tsx | 16 +- web/components/dropdowns/project.tsx | 24 +- web/components/emoji-icon-picker/emojis.json | 1090 ----------------- web/components/emoji-icon-picker/helpers.ts | 26 - web/components/emoji-icon-picker/icons.json | 607 --------- web/components/emoji-icon-picker/index.tsx | 204 --- web/components/emoji-icon-picker/types.d.ts | 15 - web/components/headers/cycle-issues.tsx | 16 +- web/components/headers/cycles.tsx | 16 +- web/components/headers/module-issues.tsx | 54 +- web/components/headers/modules-list.tsx | 13 +- web/components/headers/page-details.tsx | 12 +- web/components/headers/pages.tsx | 12 +- .../project-archived-issue-details.tsx | 13 +- .../headers/project-archived-issues.tsx | 12 +- .../headers/project-draft-issues.tsx | 12 +- web/components/headers/project-inbox.tsx | 12 +- .../headers/project-issue-details.tsx | 12 +- web/components/headers/project-issues.tsx | 18 +- web/components/headers/project-settings.tsx | 12 +- .../headers/project-view-issues.tsx | 16 +- web/components/headers/project-views.tsx | 16 +- .../filters/applied-filters/project.tsx | 16 +- .../filters/header/filters/project.tsx | 19 +- web/components/issues/issue-layouts/utils.tsx | 9 +- web/components/profile/sidebar.tsx | 30 +- web/components/project/card.tsx | 14 +- .../project/create-project-modal.tsx | 249 ++-- web/components/project/form.tsx | 71 +- web/components/project/index.ts | 1 + web/components/project/project-logo.tsx | 34 + web/components/project/sidebar-list-item.tsx | 86 +- web/helpers/emoji.helper.tsx | 9 + web/helpers/project.helper.ts | 3 + .../settings-layout/project/sidebar.tsx | 4 +- web/pages/_app.tsx | 1 + web/styles/emoji.css | 52 + yarn.lock | 7 +- 58 files changed, 1513 insertions(+), 2462 deletions(-) delete mode 100644 apiserver/plane/db/migrations/0061_alter_issuelink_url.py create mode 100644 apiserver/plane/db/migrations/0061_project_logo_props.py create mode 100644 packages/ui/src/emoji/emoji-icon-picker.tsx create mode 100644 packages/ui/src/emoji/icons-list.tsx create mode 100644 packages/ui/src/emoji/icons.ts create mode 100644 packages/ui/src/emoji/index.ts create mode 100644 space/components/common/project-logo.tsx delete mode 100644 web/components/emoji-icon-picker/emojis.json delete mode 100644 web/components/emoji-icon-picker/helpers.ts delete mode 100644 web/components/emoji-icon-picker/icons.json delete mode 100644 web/components/emoji-icon-picker/index.tsx delete mode 100644 web/components/emoji-icon-picker/types.d.ts create mode 100644 web/components/project/project-logo.tsx create mode 100644 web/styles/emoji.css diff --git a/apiserver/plane/app/serializers/project.py b/apiserver/plane/app/serializers/project.py index 999233442..6840fa8f7 100644 --- a/apiserver/plane/app/serializers/project.py +++ b/apiserver/plane/app/serializers/project.py @@ -95,8 +95,7 @@ class ProjectLiteSerializer(BaseSerializer): "identifier", "name", "cover_image", - "icon_prop", - "emoji", + "logo_props", "description", ] read_only_fields = fields diff --git a/apiserver/plane/app/views/workspace.py b/apiserver/plane/app/views/workspace.py index 84ba125ba..7c4a5db8d 100644 --- a/apiserver/plane/app/views/workspace.py +++ b/apiserver/plane/app/views/workspace.py @@ -1366,10 +1366,6 @@ class WorkspaceUserProfileEndpoint(BaseAPIView): ) .values( "id", - "name", - "identifier", - "emoji", - "icon_prop", "created_issues", "assigned_issues", "completed_issues", diff --git a/apiserver/plane/db/migrations/0061_alter_issuelink_url.py b/apiserver/plane/db/migrations/0061_alter_issuelink_url.py deleted file mode 100644 index 1aca84a80..000000000 --- a/apiserver/plane/db/migrations/0061_alter_issuelink_url.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.7 on 2024-03-01 07:16 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('db', '0060_cycle_progress_snapshot'), - ] - - operations = [ - migrations.AlterField( - model_name='issuelink', - name='url', - field=models.TextField(), - ), - ] diff --git a/apiserver/plane/db/migrations/0061_project_logo_props.py b/apiserver/plane/db/migrations/0061_project_logo_props.py new file mode 100644 index 000000000..d8752d9dd --- /dev/null +++ b/apiserver/plane/db/migrations/0061_project_logo_props.py @@ -0,0 +1,54 @@ +# Generated by Django 4.2.7 on 2024-03-03 16:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + def update_project_logo_props(apps, schema_editor): + Project = apps.get_model("db", "Project") + + bulk_update_project_logo = [] + # Iterate through projects and update logo_props + for project in Project.objects.all(): + project.logo_props["in_use"] = "emoji" if project.emoji else "icon" + project.logo_props["emoji"] = { + "value": project.emoji if project.emoji else "", + "url": "", + } + project.logo_props["icon"] = { + "name": ( + project.icon_prop.get("name", "") + if project.icon_prop + else "" + ), + "color": ( + project.icon_prop.get("color", "") + if project.icon_prop + else "" + ), + } + bulk_update_project_logo.append(project) + + # Bulk update logo_props for all projects + Project.objects.bulk_update( + bulk_update_project_logo, ["logo_props"], batch_size=1000 + ) + + dependencies = [ + ("db", "0060_cycle_progress_snapshot"), + ] + + operations = [ + migrations.AlterField( + model_name="issuelink", + name="url", + field=models.TextField(), + ), + migrations.AddField( + model_name="project", + name="logo_props", + field=models.JSONField(default=dict), + ), + migrations.RunPython(update_project_logo_props), + ] diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index b93174724..bb4885d14 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -107,6 +107,7 @@ class Project(BaseModel): close_in = models.IntegerField( default=0, validators=[MinValueValidator(0), MaxValueValidator(12)] ) + logo_props = models.JSONField(default=dict) default_state = models.ForeignKey( "db.State", on_delete=models.SET_NULL, diff --git a/packages/types/src/projects.d.ts b/packages/types/src/projects.d.ts index 86b352482..a93734186 100644 --- a/packages/types/src/projects.d.ts +++ b/packages/types/src/projects.d.ts @@ -1,12 +1,26 @@ import { EUserProjectRoles } from "constants/project"; import type { + IProjectViewProps, IUser, IUserLite, + IUserMemberLite, IWorkspace, IWorkspaceLite, TStateGroups, } from "."; +export type TProjectLogoProps = { + in_use: "emoji" | "icon"; + emoji?: { + value?: string; + url?: string; + }; + icon?: { + name?: string; + color?: string; + }; +}; + export interface IProject { archive_in: number; close_in: number; @@ -21,24 +35,13 @@ export interface IProject { default_assignee: IUser | string | null; default_state: string | null; description: string; - emoji: string | null; - emoji_and_icon: - | string - | { - name: string; - color: string; - } - | null; estimate: string | null; - icon_prop: { - name: string; - color: string; - } | null; id: string; identifier: string; is_deployed: boolean; is_favorite: boolean; is_member: boolean; + logo_props: TProjectLogoProps; member_role: EUserProjectRoles | null; members: IProjectMemberLite[]; name: string; diff --git a/packages/types/src/users.d.ts b/packages/types/src/users.d.ts index c428dc7d2..5920f0b49 100644 --- a/packages/types/src/users.d.ts +++ b/packages/types/src/users.d.ts @@ -132,11 +132,7 @@ export interface IUserProfileProjectSegregation { assigned_issues: number; completed_issues: number; created_issues: number; - emoji: string | null; - icon_prop: null; id: string; - identifier: string; - name: string; pending_issues: number; }[]; user_data: { diff --git a/packages/ui/package.json b/packages/ui/package.json index 91a010a1e..f80bcc6ae 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -23,6 +23,7 @@ "@headlessui/react": "^1.7.17", "@popperjs/core": "^2.11.8", "clsx": "^2.0.0", + "emoji-picker-react": "^4.5.16", "react-color": "^2.19.3", "react-dom": "^18.2.0", "react-popper": "^2.3.0", diff --git a/packages/ui/src/emoji/emoji-icon-picker.tsx b/packages/ui/src/emoji/emoji-icon-picker.tsx new file mode 100644 index 000000000..42c367938 --- /dev/null +++ b/packages/ui/src/emoji/emoji-icon-picker.tsx @@ -0,0 +1,169 @@ +import React, { useState } from "react"; +import { usePopper } from "react-popper"; +import EmojiPicker, { EmojiClickData, Theme } from "emoji-picker-react"; +import { Popover, Tab } from "@headlessui/react"; +import { Placement } from "@popperjs/core"; +// components +import { IconsList } from "./icons-list"; +// helpers +import { cn } from "../../helpers"; + +export enum EmojiIconPickerTypes { + EMOJI = "emoji", + ICON = "icon", +} + +type TChangeHandlerProps = + | { + type: EmojiIconPickerTypes.EMOJI; + value: EmojiClickData; + } + | { + type: EmojiIconPickerTypes.ICON; + value: { + name: string; + color: string; + }; + }; + +export type TCustomEmojiPicker = { + buttonClassName?: string; + className?: string; + closeOnSelect?: boolean; + defaultIconColor?: string; + defaultOpen?: EmojiIconPickerTypes; + disabled?: boolean; + dropdownClassName?: string; + label: React.ReactNode; + onChange: (value: TChangeHandlerProps) => void; + placement?: Placement; + searchPlaceholder?: string; + theme?: Theme; +}; + +const TABS_LIST = [ + { + key: EmojiIconPickerTypes.EMOJI, + title: "Emojis", + }, + { + key: EmojiIconPickerTypes.ICON, + title: "Icons", + }, +]; + +export const CustomEmojiIconPicker: React.FC = (props) => { + const { + buttonClassName, + className, + closeOnSelect = true, + defaultIconColor = "#5f5f5f", + defaultOpen = EmojiIconPickerTypes.EMOJI, + disabled = false, + dropdownClassName, + label, + onChange, + placement = "bottom-start", + searchPlaceholder = "Search", + theme, + } = props; + // refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement, + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 20, + }, + }, + ], + }); + + return ( + + {({ close }) => ( + <> + + + + +
+ tab.key === defaultOpen)} + > + + {TABS_LIST.map((tab) => ( + + cn("py-1 text-sm rounded border border-custom-border-200", { + "bg-custom-background-80": selected, + "hover:bg-custom-background-90 focus:bg-custom-background-90": !selected, + }) + } + > + {tab.title} + + ))} + + + + { + onChange({ + type: EmojiIconPickerTypes.EMOJI, + value: val, + }); + if (closeOnSelect) close(); + }} + height="20rem" + width="100%" + theme={theme} + searchPlaceholder={searchPlaceholder} + previewConfig={{ + showPreview: false, + }} + /> + + + { + onChange({ + type: EmojiIconPickerTypes.ICON, + value: val, + }); + if (closeOnSelect) close(); + }} + /> + + + +
+
+ + )} +
+ ); +}; diff --git a/packages/ui/src/emoji/icons-list.tsx b/packages/ui/src/emoji/icons-list.tsx new file mode 100644 index 000000000..f55da881b --- /dev/null +++ b/packages/ui/src/emoji/icons-list.tsx @@ -0,0 +1,110 @@ +import React, { useEffect, useState } from "react"; +// components +import { Input } from "../form-fields"; +// helpers +import { cn } from "../../helpers"; +// constants +import { MATERIAL_ICONS_LIST } from "./icons"; + +type TIconsListProps = { + defaultColor: string; + onChange: (val: { name: string; color: string }) => void; +}; + +const DEFAULT_COLORS = ["#ff6b00", "#8cc1ff", "#fcbe1d", "#18904f", "#adf672", "#05c3ff", "#5f5f5f"]; + +export const IconsList: React.FC = (props) => { + const { defaultColor, onChange } = props; + // states + const [activeColor, setActiveColor] = useState(defaultColor); + const [showHexInput, setShowHexInput] = useState(false); + const [hexValue, setHexValue] = useState(""); + + useEffect(() => { + if (DEFAULT_COLORS.includes(defaultColor.toLowerCase())) setShowHexInput(false); + else { + setHexValue(defaultColor.slice(1, 7)); + setShowHexInput(true); + } + }, [defaultColor]); + + return ( + <> +
+ {showHexInput ? ( +
+ + HEX + # + { + const value = e.target.value; + setHexValue(value); + if (/^[0-9A-Fa-f]{6}$/.test(value)) setActiveColor(`#${value}`); + }} + className="flex-grow pl-0 text-xs text-custom-text-200" + mode="true-transparent" + autoFocus + /> +
+ ) : ( + DEFAULT_COLORS.map((curCol) => ( + + )) + )} + +
+
+ {MATERIAL_ICONS_LIST.map((icon) => ( + + ))} +
+ + ); +}; diff --git a/packages/ui/src/emoji/icons.ts b/packages/ui/src/emoji/icons.ts new file mode 100644 index 000000000..72aacf18b --- /dev/null +++ b/packages/ui/src/emoji/icons.ts @@ -0,0 +1,605 @@ +export const MATERIAL_ICONS_LIST = [ + { + name: "search", + }, + { + name: "home", + }, + { + name: "menu", + }, + { + name: "close", + }, + { + name: "settings", + }, + { + name: "done", + }, + { + name: "check_circle", + }, + { + name: "favorite", + }, + { + name: "add", + }, + { + name: "delete", + }, + { + name: "arrow_back", + }, + { + name: "star", + }, + { + name: "logout", + }, + { + name: "add_circle", + }, + { + name: "cancel", + }, + { + name: "arrow_drop_down", + }, + { + name: "more_vert", + }, + { + name: "check", + }, + { + name: "check_box", + }, + { + name: "toggle_on", + }, + { + name: "open_in_new", + }, + { + name: "refresh", + }, + { + name: "login", + }, + { + name: "radio_button_unchecked", + }, + { + name: "more_horiz", + }, + { + name: "apps", + }, + { + name: "radio_button_checked", + }, + { + name: "download", + }, + { + name: "remove", + }, + { + name: "toggle_off", + }, + { + name: "bolt", + }, + { + name: "arrow_upward", + }, + { + name: "filter_list", + }, + { + name: "delete_forever", + }, + { + name: "autorenew", + }, + { + name: "key", + }, + { + name: "sort", + }, + { + name: "sync", + }, + { + name: "add_box", + }, + { + name: "block", + }, + { + name: "restart_alt", + }, + { + name: "menu_open", + }, + { + name: "shopping_cart_checkout", + }, + { + name: "expand_circle_down", + }, + { + name: "backspace", + }, + { + name: "undo", + }, + { + name: "done_all", + }, + { + name: "do_not_disturb_on", + }, + { + name: "open_in_full", + }, + { + name: "double_arrow", + }, + { + name: "sync_alt", + }, + { + name: "zoom_in", + }, + { + name: "done_outline", + }, + { + name: "drag_indicator", + }, + { + name: "fullscreen", + }, + { + name: "star_half", + }, + { + name: "settings_accessibility", + }, + { + name: "reply", + }, + { + name: "exit_to_app", + }, + { + name: "unfold_more", + }, + { + name: "library_add", + }, + { + name: "cached", + }, + { + name: "select_check_box", + }, + { + name: "terminal", + }, + { + name: "change_circle", + }, + { + name: "disabled_by_default", + }, + { + name: "swap_horiz", + }, + { + name: "swap_vert", + }, + { + name: "app_registration", + }, + { + name: "download_for_offline", + }, + { + name: "close_fullscreen", + }, + { + name: "file_open", + }, + { + name: "minimize", + }, + { + name: "open_with", + }, + { + name: "dataset", + }, + { + name: "add_task", + }, + { + name: "start", + }, + { + name: "keyboard_voice", + }, + { + name: "create_new_folder", + }, + { + name: "forward", + }, + { + name: "download", + }, + { + name: "settings_applications", + }, + { + name: "compare_arrows", + }, + { + name: "redo", + }, + { + name: "zoom_out", + }, + { + name: "publish", + }, + { + name: "html", + }, + { + name: "token", + }, + { + name: "switch_access_shortcut", + }, + { + name: "fullscreen_exit", + }, + { + name: "sort_by_alpha", + }, + { + name: "delete_sweep", + }, + { + name: "indeterminate_check_box", + }, + { + name: "view_timeline", + }, + { + name: "settings_backup_restore", + }, + { + name: "arrow_drop_down_circle", + }, + { + name: "assistant_navigation", + }, + { + name: "sync_problem", + }, + { + name: "clear_all", + }, + { + name: "density_medium", + }, + { + name: "heart_plus", + }, + { + name: "filter_alt_off", + }, + { + name: "expand", + }, + { + name: "subdirectory_arrow_right", + }, + { + name: "download_done", + }, + { + name: "arrow_outward", + }, + { + name: "123", + }, + { + name: "swipe_left", + }, + { + name: "auto_mode", + }, + { + name: "saved_search", + }, + { + name: "place_item", + }, + { + name: "system_update_alt", + }, + { + name: "javascript", + }, + { + name: "search_off", + }, + { + name: "output", + }, + { + name: "select_all", + }, + { + name: "fit_screen", + }, + { + name: "swipe_up", + }, + { + name: "dynamic_form", + }, + { + name: "hide_source", + }, + { + name: "swipe_right", + }, + { + name: "switch_access_shortcut_add", + }, + { + name: "browse_gallery", + }, + { + name: "css", + }, + { + name: "density_small", + }, + { + name: "assistant_direction", + }, + { + name: "check_small", + }, + { + name: "youtube_searched_for", + }, + { + name: "move_up", + }, + { + name: "swap_horizontal_circle", + }, + { + name: "data_thresholding", + }, + { + name: "install_mobile", + }, + { + name: "move_down", + }, + { + name: "dataset_linked", + }, + { + name: "keyboard_command_key", + }, + { + name: "view_kanban", + }, + { + name: "swipe_down", + }, + { + name: "key_off", + }, + { + name: "transcribe", + }, + { + name: "send_time_extension", + }, + { + name: "swipe_down_alt", + }, + { + name: "swipe_left_alt", + }, + { + name: "swipe_right_alt", + }, + { + name: "swipe_up_alt", + }, + { + name: "keyboard_option_key", + }, + { + name: "cycle", + }, + { + name: "rebase", + }, + { + name: "rebase_edit", + }, + { + name: "empty_dashboard", + }, + { + name: "magic_exchange", + }, + { + name: "acute", + }, + { + name: "point_scan", + }, + { + name: "step_into", + }, + { + name: "cheer", + }, + { + name: "emoticon", + }, + { + name: "explosion", + }, + { + name: "water_bottle", + }, + { + name: "weather_hail", + }, + { + name: "syringe", + }, + { + name: "pill", + }, + { + name: "genetics", + }, + { + name: "allergy", + }, + { + name: "medical_mask", + }, + { + name: "body_fat", + }, + { + name: "barefoot", + }, + { + name: "infrared", + }, + { + name: "wrist", + }, + { + name: "metabolism", + }, + { + name: "conditions", + }, + { + name: "taunt", + }, + { + name: "altitude", + }, + { + name: "tibia", + }, + { + name: "footprint", + }, + { + name: "eyeglasses", + }, + { + name: "man_3", + }, + { + name: "woman_2", + }, + { + name: "rheumatology", + }, + { + name: "tornado", + }, + { + name: "landslide", + }, + { + name: "foggy", + }, + { + name: "severe_cold", + }, + { + name: "tsunami", + }, + { + name: "vape_free", + }, + { + name: "sign_language", + }, + { + name: "emoji_symbols", + }, + { + name: "clear_night", + }, + { + name: "emoji_food_beverage", + }, + { + name: "hive", + }, + { + name: "thunderstorm", + }, + { + name: "communication", + }, + { + name: "rocket", + }, + { + name: "pets", + }, + { + name: "public", + }, + { + name: "quiz", + }, + { + name: "mood", + }, + { + name: "gavel", + }, + { + name: "eco", + }, + { + name: "diamond", + }, + { + name: "forest", + }, + { + name: "rainy", + }, + { + name: "skull", + }, +]; diff --git a/packages/ui/src/emoji/index.ts b/packages/ui/src/emoji/index.ts new file mode 100644 index 000000000..973454139 --- /dev/null +++ b/packages/ui/src/emoji/index.ts @@ -0,0 +1 @@ +export * from "./emoji-icon-picker"; diff --git a/packages/ui/src/form-fields/input.tsx b/packages/ui/src/form-fields/input.tsx index 6688d6778..f73467621 100644 --- a/packages/ui/src/form-fields/input.tsx +++ b/packages/ui/src/form-fields/input.tsx @@ -1,4 +1,6 @@ import * as React from "react"; +// helpers +import { cn } from "../../helpers"; export interface InputProps extends React.InputHTMLAttributes { mode?: "primary" | "transparent" | "true-transparent"; @@ -16,17 +18,20 @@ const Input = React.forwardRef((props, ref) => { ref={ref} type={type} name={name} - className={`block rounded-md bg-transparent text-sm placeholder-custom-text-400 focus:outline-none ${ - mode === "primary" - ? "rounded-md border-[0.5px] border-custom-border-200" - : mode === "transparent" - ? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-custom-primary" - : mode === "true-transparent" - ? "rounded border-none bg-transparent ring-0" - : "" - } ${hasError ? "border-red-500" : ""} ${hasError && mode === "primary" ? "bg-red-500/20" : ""} ${ - inputSize === "sm" ? "px-3 py-2" : inputSize === "md" ? "p-3" : "" - } ${className}`} + className={cn( + `block rounded-md bg-transparent text-sm placeholder-custom-text-400 focus:outline-none ${ + mode === "primary" + ? "rounded-md border-[0.5px] border-custom-border-200" + : mode === "transparent" + ? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-custom-primary" + : mode === "true-transparent" + ? "rounded border-none bg-transparent ring-0" + : "" + } ${hasError ? "border-red-500" : ""} ${hasError && mode === "primary" ? "bg-red-500/20" : ""} ${ + inputSize === "sm" ? "px-3 py-2" : inputSize === "md" ? "p-3" : "" + }`, + className + )} {...rest} /> ); diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 218d375fa..24b76c3e0 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -2,6 +2,7 @@ export * from "./avatar"; export * from "./breadcrumbs"; export * from "./badge"; export * from "./button"; +export * from "./emoji"; export * from "./dropdowns"; export * from "./form-fields"; export * from "./icons"; diff --git a/space/components/common/index.ts b/space/components/common/index.ts index f1c0b088e..36cc3c898 100644 --- a/space/components/common/index.ts +++ b/space/components/common/index.ts @@ -1 +1,2 @@ export * from "./latest-feature-block"; +export * from "./project-logo"; diff --git a/space/components/common/project-logo.tsx b/space/components/common/project-logo.tsx new file mode 100644 index 000000000..3d5887b28 --- /dev/null +++ b/space/components/common/project-logo.tsx @@ -0,0 +1,34 @@ +// helpers +import { cn } from "helpers/common.helper"; +// types +import { TProjectLogoProps } from "@plane/types"; + +type Props = { + className?: string; + logo: TProjectLogoProps; +}; + +export const ProjectLogo: React.FC = (props) => { + const { className, logo } = props; + + if (logo.in_use === "icon" && logo.icon) + return ( + + {logo.icon.name} + + ); + + if (logo.in_use === "emoji" && logo.emoji) + return ( + + {logo.emoji.value?.split("-").map((emoji) => String.fromCodePoint(parseInt(emoji, 10)))} + + ); + + return ; +}; diff --git a/space/components/issues/navbar/index.tsx b/space/components/issues/navbar/index.tsx index 0bc493b16..feb11ed13 100644 --- a/space/components/issues/navbar/index.tsx +++ b/space/components/issues/navbar/index.tsx @@ -1,15 +1,12 @@ import { useEffect } from "react"; - import Link from "next/link"; import { useRouter } from "next/router"; - -// mobx import { observer } from "mobx-react-lite"; // components -// import { NavbarSearch } from "./search"; import { NavbarIssueBoardView } from "./issue-board-view"; import { NavbarTheme } from "./theme"; import { IssueFiltersDropdown } from "components/issues/filters"; +import { ProjectLogo } from "components/common"; // ui import { Avatar, Button } from "@plane/ui"; import { Briefcase } from "lucide-react"; @@ -19,18 +16,6 @@ import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; import { TIssueBoardKeys } from "types/issue"; -const renderEmoji = (emoji: string | { name: string; color: string }) => { - if (!emoji) return; - - if (typeof emoji === "object") - return ( - - {emoji.name} - - ); - else return isNaN(parseInt(emoji)) ? emoji : String.fromCodePoint(parseInt(emoji)); -}; - const IssueNavbar = observer(() => { const { project: projectStore, @@ -123,27 +108,15 @@ const IssueNavbar = observer(() => {
{/* project detail */}
-
- {projectStore.project ? ( - projectStore.project?.emoji ? ( - - {renderEmoji(projectStore.project.emoji)} - - ) : projectStore.project?.icon_prop ? ( -
- {renderEmoji(projectStore.project.icon_prop)} -
- ) : ( - - {projectStore.project?.name.charAt(0)} - - ) - ) : ( - - - - )} -
+ {projectStore.project ? ( + + + + ) : ( + + + + )}
{projectStore?.project?.name || `...`}
diff --git a/space/types/project.ts b/space/types/project.ts index e0e1bba9e..7e81d366c 100644 --- a/space/types/project.ts +++ b/space/types/project.ts @@ -1,3 +1,5 @@ +import { TProjectLogoProps } from "@plane/types"; + export interface IWorkspace { id: string; name: string; @@ -9,10 +11,8 @@ export interface IProject { identifier: string; name: string; description: string; - icon: string; cover_image: string | null; - icon_prop: string | null; - emoji: string | null; + logo_props: TProjectLogoProps; } export interface IProjectSettings { diff --git a/web/components/analytics/custom-analytics/sidebar/projects-list.tsx b/web/components/analytics/custom-analytics/sidebar/projects-list.tsx index 9a0eec227..31812cb00 100644 --- a/web/components/analytics/custom-analytics/sidebar/projects-list.tsx +++ b/web/components/analytics/custom-analytics/sidebar/projects-list.tsx @@ -3,9 +3,9 @@ import { observer } from "mobx-react-lite"; // icons import { Contrast, LayoutGrid, Users } from "lucide-react"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; import { truncateText } from "helpers/string.helper"; import { useProject } from "hooks/store"; +import { ProjectLogo } from "components/project"; type Props = { projectIds: string[]; @@ -28,15 +28,9 @@ export const CustomAnalyticsSidebarProjectsList: React.FC = observer((pro return (
- {project.emoji ? ( - {renderEmoji(project.emoji)} - ) : project.icon_prop ? ( -
{renderEmoji(project.icon_prop)}
- ) : ( - - {project?.name.charAt(0)} - - )} +
+ +

{truncateText(project.name, 20)}

({project.identifier}) diff --git a/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx b/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx index fb9ab90fa..26f97e8f9 100644 --- a/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx +++ b/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx @@ -3,8 +3,9 @@ import { useRouter } from "next/router"; // hooks import { NETWORK_CHOICES } from "constants/project"; import { renderFormattedDate } from "helpers/date-time.helper"; -import { renderEmoji } from "helpers/emoji.helper"; import { useCycle, useMember, useModule, useProject } from "hooks/store"; +// components +import { ProjectLogo } from "components/project"; // helpers // constants @@ -81,15 +82,9 @@ export const CustomAnalyticsSidebarHeader = observer(() => { ) : (
- {projectDetails?.emoji ? ( -
{renderEmoji(projectDetails.emoji)}
- ) : projectDetails?.icon_prop ? ( -
- {renderEmoji(projectDetails.icon_prop)} -
- ) : ( - - {projectDetails?.name.charAt(0)} + {projectDetails && ( + + )}

{projectDetails?.name}

diff --git a/web/components/dashboard/widgets/recent-projects.tsx b/web/components/dashboard/widgets/recent-projects.tsx index 72129df3f..22e561ac8 100644 --- a/web/components/dashboard/widgets/recent-projects.tsx +++ b/web/components/dashboard/widgets/recent-projects.tsx @@ -7,13 +7,13 @@ import { Avatar, AvatarGroup } from "@plane/ui"; import { WidgetLoader, WidgetProps } from "components/dashboard/widgets"; import { PROJECT_BACKGROUND_COLORS } from "constants/dashboard"; import { EUserWorkspaceRoles } from "constants/workspace"; -import { renderEmoji } from "helpers/emoji.helper"; import { useApplication, useEventTracker, useDashboard, useProject, useUser } from "hooks/store"; // components // ui // helpers // types import { TRecentProjectsWidgetResponse } from "@plane/types"; +import { ProjectLogo } from "components/project"; // constants const WIDGET_KEY = "recent_projects"; @@ -38,17 +38,9 @@ const ProjectListItem: React.FC = observer((props) => {
- {projectDetails.emoji ? ( - - {renderEmoji(projectDetails.emoji)} - - ) : projectDetails.icon_prop ? ( -
{renderEmoji(projectDetails.icon_prop)}
- ) : ( - - {projectDetails.name.charAt(0)} - - )} +
+ +
diff --git a/web/components/dropdowns/project.tsx b/web/components/dropdowns/project.tsx index 05b455e5e..719b89802 100644 --- a/web/components/dropdowns/project.tsx +++ b/web/components/dropdowns/project.tsx @@ -5,12 +5,12 @@ import { Combobox } from "@headlessui/react"; import { Check, ChevronDown, Search } from "lucide-react"; // hooks import { cn } from "helpers/common.helper"; -import { renderEmoji } from "helpers/emoji.helper"; import { useProject } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components import { DropdownButton } from "./buttons"; +import { ProjectLogo } from "components/project"; // helpers // types import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; @@ -77,13 +77,11 @@ export const ProjectDropdown: React.FC = observer((props) => { query: `${projectDetails?.name}`, content: (
- - {projectDetails?.emoji - ? renderEmoji(projectDetails?.emoji) - : projectDetails?.icon_prop - ? renderEmoji(projectDetails?.icon_prop) - : null} - + {projectDetails && ( + + + + )} {projectDetails?.name}
), @@ -169,13 +167,9 @@ export const ProjectDropdown: React.FC = observer((props) => { showTooltip={showTooltip} variant={buttonVariant} > - {!hideIcon && ( - - {selectedProject?.emoji - ? renderEmoji(selectedProject?.emoji) - : selectedProject?.icon_prop - ? renderEmoji(selectedProject?.icon_prop) - : null} + {!hideIcon && selectedProject && ( + + )} {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && ( diff --git a/web/components/emoji-icon-picker/emojis.json b/web/components/emoji-icon-picker/emojis.json deleted file mode 100644 index 73b9b800f..000000000 --- a/web/components/emoji-icon-picker/emojis.json +++ /dev/null @@ -1,1090 +0,0 @@ -[ - "8986", - "8987", - "9193", - "9194", - "9195", - "9196", - "9197", - "9198", - "9199", - "9200", - "9201", - "9202", - "9203", - "9208", - "9209", - "9210", - "9410", - "9748", - "9749", - "9757", - "9800", - "9801", - "9802", - "9803", - "9804", - "9805", - "9806", - "9807", - "9808", - "9809", - "9810", - "9811", - "9823", - "9855", - "9875", - "9889", - "9898", - "9899", - "9917", - "9918", - "9924", - "9925", - "9934", - "9935", - "9937", - "9939", - "9940", - "9961", - "9962", - "9968", - "9969", - "9970", - "9971", - "9972", - "9973", - "9975", - "9976", - "9977", - "9978", - "9981", - "9986", - "9989", - "9992", - "9993", - "9994", - "9995", - "9996", - "9997", - "9999", - "10002", - "10004", - "10006", - "10013", - "10017", - "10024", - "10035", - "10036", - "10052", - "10055", - "10060", - "10062", - "10067", - "10068", - "10069", - "10071", - "10083", - "10084", - "10133", - "10134", - "10135", - "10145", - "10160", - "10175", - "10548", - "10549", - "11013", - "11014", - "11015", - "11035", - "11036", - "11088", - "11093", - "12336", - "12349", - "12951", - "12953", - "126980", - "127183", - "127344", - "127345", - "127358", - "127359", - "127374", - "127377", - "127378", - "127379", - "127380", - "127381", - "127382", - "127383", - "127384", - "127385", - "127386", - "127489", - "127490", - "127514", - "127535", - "127538", - "127539", - "127540", - "127541", - "127542", - "127543", - "127544", - "127545", - "127546", - "127568", - "127569", - "127744", - "127745", - "127746", - "127747", - "127748", - "127749", - "127750", - "127751", - "127752", - "127753", - "127754", - "127755", - "127756", - "127757", - "127758", - "127759", - "127760", - "127761", - "127762", - "127763", - "127764", - "127765", - "127766", - "127767", - "127768", - "127769", - "127770", - "127771", - "127772", - "127773", - "127774", - "127775", - "127776", - "127777", - "127780", - "127781", - "127782", - "127783", - "127784", - "127785", - "127786", - "127787", - "127788", - "127789", - "127790", - "127791", - "127792", - "127793", - "127794", - "127795", - "127796", - "127797", - "127798", - "127799", - "127800", - "127801", - "127802", - "127803", - "127804", - "127805", - "127806", - "127807", - "127808", - "127809", - "127810", - "127811", - "127812", - "127813", - "127814", - "127815", - "127816", - "127817", - "127818", - "127819", - "127820", - "127821", - "127822", - "127823", - "127824", - "127825", - "127826", - "127827", - "127828", - "127829", - "127830", - "127831", - "127832", - "127833", - "127834", - "127835", - "127836", - "127837", - "127838", - "127839", - "127840", - "127841", - "127842", - "127843", - "127844", - "127845", - "127846", - "127847", - "127848", - "127849", - "127850", - "127851", - "127852", - "127853", - "127854", - "127855", - "127856", - "127857", - "127858", - "127859", - "127860", - "127861", - "127862", - "127863", - "127864", - "127865", - "127866", - "127867", - "127868", - "127869", - "127870", - "127871", - "127872", - "127873", - "127874", - "127875", - "127876", - "127877", - "127878", - "127879", - "127880", - "127881", - "127882", - "127883", - "127884", - "127885", - "127886", - "127887", - "127888", - "127889", - "127890", - "127891", - "127894", - "127895", - "127897", - "127898", - "127899", - "127902", - "127903", - "127904", - "127905", - "127906", - "127907", - "127908", - "127909", - "127910", - "127911", - "127912", - "127913", - "127914", - "127915", - "127916", - "127917", - "127918", - "127919", - "127920", - "127921", - "127922", - "127923", - "127924", - "127925", - "127926", - "127927", - "127928", - "127929", - "127930", - "127931", - "127932", - "127933", - "127934", - "127935", - "127936", - "127937", - "127938", - "127939", - "127940", - "127941", - "127942", - "127943", - "127944", - "127945", - "127946", - "127947", - "127948", - "127949", - "127950", - "127951", - "127952", - "127953", - "127954", - "127955", - "127956", - "127957", - "127958", - "127959", - "127960", - "127961", - "127962", - "127963", - "127964", - "127965", - "127966", - "127967", - "127968", - "127969", - "127970", - "127971", - "127972", - "127973", - "127974", - "127975", - "127976", - "127977", - "127978", - "127979", - "127980", - "127981", - "127982", - "127983", - "127984", - "127987", - "127988", - "127989", - "127991", - "127992", - "127993", - "127994", - "127995", - "127996", - "127997", - "127998", - "127999", - "128000", - "128001", - "128002", - "128003", - "128004", - "128005", - "128006", - "128007", - "128008", - "128009", - "128010", - "128011", - "128012", - "128013", - "128014", - "128015", - "128016", - "128017", - "128018", - "128019", - "128020", - "128021", - "128022", - "128023", - "128024", - "128025", - "128026", - "128027", - "128028", - "128029", - "128030", - "128031", - "128032", - "128033", - "128034", - "128035", - "128036", - "128037", - "128038", - "128039", - "128040", - "128041", - "128042", - "128043", - "128044", - "128045", - "128046", - "128047", - "128048", - "128049", - "128050", - "128051", - "128052", - "128053", - "128054", - "128055", - "128056", - "128057", - "128058", - "128059", - "128060", - "128061", - "128062", - "128063", - "128064", - "128065", - "128066", - "128067", - "128068", - "128069", - "128070", - "128071", - "128072", - "128073", - "128074", - "128075", - "128076", - "128077", - "128078", - "128079", - "128080", - "128081", - "128082", - "128083", - "128084", - "128085", - "128086", - "128087", - "128088", - "128089", - "128090", - "128091", - "128092", - "128093", - "128094", - "128095", - "128096", - "128097", - "128098", - "128099", - "128100", - "128101", - "128102", - "128103", - "128104", - "128105", - "128106", - "128107", - "128108", - "128109", - "128110", - "128111", - "128112", - "128113", - "128114", - "128115", - "128116", - "128117", - "128118", - "128119", - "128120", - "128121", - "128122", - "128123", - "128124", - "128125", - "128126", - "128127", - "128128", - "128129", - "128130", - "128131", - "128132", - "128133", - "128134", - "128135", - "128136", - "128137", - "128138", - "128139", - "128140", - "128141", - "128142", - "128143", - "128144", - "128145", - "128146", - "128147", - "128148", - "128149", - "128150", - "128151", - "128152", - "128153", - "128154", - "128155", - "128156", - "128157", - "128158", - "128159", - "128160", - "128161", - "128162", - "128163", - "128164", - "128165", - "128166", - "128167", - "128168", - "128169", - "128170", - "128171", - "128172", - "128173", - "128174", - "128175", - "128176", - "128177", - "128178", - "128179", - "128180", - "128181", - "128182", - "128183", - "128184", - "128185", - "128186", - "128187", - "128188", - "128189", - "128190", - "128191", - "128192", - "128193", - "128194", - "128195", - "128196", - "128197", - "128198", - "128199", - "128200", - "128201", - "128202", - "128203", - "128204", - "128205", - "128206", - "128207", - "128208", - "128209", - "128210", - "128211", - "128212", - "128213", - "128214", - "128215", - "128216", - "128217", - "128218", - "128219", - "128220", - "128221", - "128222", - "128223", - "128224", - "128225", - "128226", - "128227", - "128228", - "128229", - "128230", - "128231", - "128232", - "128233", - "128234", - "128235", - "128236", - "128237", - "128238", - "128239", - "128240", - "128241", - "128242", - "128243", - "128244", - "128245", - "128246", - "128247", - "128248", - "128249", - "128250", - "128251", - "128252", - "128253", - "128255", - "128256", - "128257", - "128258", - "128259", - "128260", - "128261", - "128262", - "128263", - "128264", - "128265", - "128266", - "128267", - "128268", - "128269", - "128270", - "128271", - "128272", - "128273", - "128274", - "128275", - "128276", - "128277", - "128278", - "128279", - "128280", - "128281", - "128282", - "128283", - "128284", - "128285", - "128286", - "128287", - "128288", - "128289", - "128290", - "128291", - "128292", - "128293", - "128294", - "128295", - "128296", - "128297", - "128298", - "128299", - "128300", - "128301", - "128302", - "128303", - "128304", - "128305", - "128306", - "128307", - "128308", - "128309", - "128310", - "128311", - "128312", - "128313", - "128314", - "128315", - "128316", - "128317", - "128329", - "128330", - "128331", - "128332", - "128333", - "128334", - "128336", - "128337", - "128338", - "128339", - "128340", - "128341", - "128342", - "128343", - "128344", - "128345", - "128346", - "128347", - "128348", - "128349", - "128350", - "128351", - "128352", - "128353", - "128354", - "128355", - "128356", - "128357", - "128358", - "128359", - "128367", - "128368", - "128371", - "128372", - "128373", - "128374", - "128375", - "128376", - "128377", - "128378", - "128391", - "128394", - "128395", - "128396", - "128397", - "128400", - "128405", - "128406", - "128420", - "128421", - "128424", - "128433", - "128434", - "128444", - "128450", - "128451", - "128452", - "128465", - "128466", - "128467", - "128476", - "128477", - "128478", - "128481", - "128483", - "128488", - "128495", - "128499", - "128506", - "128507", - "128508", - "128509", - "128510", - "128511", - "128512", - "128513", - "128514", - "128515", - "128516", - "128517", - "128518", - "128519", - "128520", - "128521", - "128522", - "128523", - "128524", - "128525", - "128526", - "128527", - "128528", - "128529", - "128530", - "128531", - "128532", - "128533", - "128534", - "128535", - "128536", - "128537", - "128538", - "128539", - "128540", - "128541", - "128542", - "128543", - "128544", - "128545", - "128546", - "128547", - "128548", - "128549", - "128550", - "128551", - "128552", - "128553", - "128554", - "128555", - "128556", - "128557", - "128558", - "128559", - "128560", - "128561", - "128562", - "128563", - "128564", - "128565", - "128566", - "128567", - "128568", - "128569", - "128570", - "128571", - "128572", - "128573", - "128574", - "128575", - "128576", - "128577", - "128578", - "128579", - "128580", - "128581", - "128582", - "128583", - "128584", - "128585", - "128586", - "128587", - "128588", - "128589", - "128590", - "128591", - "128640", - "128641", - "128642", - "128643", - "128644", - "128645", - "128646", - "128647", - "128648", - "128649", - "128650", - "128651", - "128652", - "128653", - "128654", - "128655", - "128656", - "128657", - "128658", - "128659", - "128660", - "128661", - "128662", - "128663", - "128664", - "128665", - "128666", - "128667", - "128668", - "128669", - "128670", - "128671", - "128672", - "128673", - "128674", - "128675", - "128676", - "128677", - "128678", - "128679", - "128680", - "128681", - "128682", - "128683", - "128684", - "128685", - "128686", - "128687", - "128688", - "128689", - "128690", - "128691", - "128692", - "128693", - "128694", - "128695", - "128696", - "128697", - "128698", - "128699", - "128700", - "128701", - "128702", - "128703", - "128704", - "128705", - "128706", - "128707", - "128708", - "128709", - "128715", - "128716", - "128717", - "128718", - "128719", - "128720", - "128721", - "128722", - "128736", - "128737", - "128738", - "128739", - "128740", - "128741", - "128745", - "128747", - "128748", - "128752", - "128755", - "128756", - "128757", - "128758", - "128759", - "128760", - "128761", - "128762", - "129296", - "129297", - "129298", - "129299", - "129300", - "129301", - "129302", - "129303", - "129304", - "129305", - "129306", - "129307", - "129308", - "129309", - "129310", - "129311", - "129312", - "129313", - "129314", - "129315", - "129316", - "129317", - "129318", - "129319", - "129320", - "129321", - "129322", - "129323", - "129324", - "129325", - "129326", - "129327", - "129328", - "129329", - "129330", - "129331", - "129332", - "129333", - "129334", - "129335", - "129336", - "129337", - "129338", - "129340", - "129341", - "129342", - "129344", - "129345", - "129346", - "129347", - "129348", - "129349", - "129351", - "129352", - "129353", - "129354", - "129355", - "129356", - "129357", - "129358", - "129359", - "129360", - "129361", - "129362", - "129363", - "129364", - "129365", - "129366", - "129367", - "129368", - "129369", - "129370", - "129371", - "129372", - "129373", - "129374", - "129375", - "129376", - "129377", - "129378", - "129379", - "129380", - "129381", - "129382", - "129383", - "129384", - "129385", - "129386", - "129387", - "129408", - "129409", - "129410", - "129411", - "129412", - "129413", - "129414", - "129415", - "129416", - "129417", - "129418", - "129419", - "129420", - "129421", - "129422", - "129423", - "129424", - "129425", - "129426", - "129427", - "129428", - "129429", - "129430", - "129431", - "129472", - "129488", - "129489", - "129490", - "129491", - "129492", - "129493", - "129494", - "129495", - "129496", - "129497", - "129498", - "129499", - "129500", - "129501", - "129502", - "129503", - "129504", - "129505", - "129506", - "129507", - "129508", - "129509", - "129510" -] diff --git a/web/components/emoji-icon-picker/helpers.ts b/web/components/emoji-icon-picker/helpers.ts deleted file mode 100644 index ab59a7b07..000000000 --- a/web/components/emoji-icon-picker/helpers.ts +++ /dev/null @@ -1,26 +0,0 @@ -export const saveRecentEmoji = (emoji: string) => { - const recentEmojis = localStorage.getItem("recentEmojis"); - if (recentEmojis) { - const recentEmojisArray = recentEmojis.split(","); - if (recentEmojisArray.includes(emoji)) { - const index = recentEmojisArray.indexOf(emoji); - recentEmojisArray.splice(index, 1); - } - recentEmojisArray.unshift(emoji); - if (recentEmojisArray.length > 18) { - recentEmojisArray.pop(); - } - localStorage.setItem("recentEmojis", recentEmojisArray.join(",")); - } else { - localStorage.setItem("recentEmojis", emoji); - } -}; - -export const getRecentEmojis = () => { - const recentEmojis = localStorage.getItem("recentEmojis"); - if (recentEmojis) { - const recentEmojisArray = recentEmojis.split(","); - return recentEmojisArray; - } - return []; -}; diff --git a/web/components/emoji-icon-picker/icons.json b/web/components/emoji-icon-picker/icons.json deleted file mode 100644 index f844f22d4..000000000 --- a/web/components/emoji-icon-picker/icons.json +++ /dev/null @@ -1,607 +0,0 @@ -{ - "material_rounded": [ - { - "name": "search" - }, - { - "name": "home" - }, - { - "name": "menu" - }, - { - "name": "close" - }, - { - "name": "settings" - }, - { - "name": "done" - }, - { - "name": "check_circle" - }, - { - "name": "favorite" - }, - { - "name": "add" - }, - { - "name": "delete" - }, - { - "name": "arrow_back" - }, - { - "name": "star" - }, - { - "name": "logout" - }, - { - "name": "add_circle" - }, - { - "name": "cancel" - }, - { - "name": "arrow_drop_down" - }, - { - "name": "more_vert" - }, - { - "name": "check" - }, - { - "name": "check_box" - }, - { - "name": "toggle_on" - }, - { - "name": "open_in_new" - }, - { - "name": "refresh" - }, - { - "name": "login" - }, - { - "name": "radio_button_unchecked" - }, - { - "name": "more_horiz" - }, - { - "name": "apps" - }, - { - "name": "radio_button_checked" - }, - { - "name": "download" - }, - { - "name": "remove" - }, - { - "name": "toggle_off" - }, - { - "name": "bolt" - }, - { - "name": "arrow_upward" - }, - { - "name": "filter_list" - }, - { - "name": "delete_forever" - }, - { - "name": "autorenew" - }, - { - "name": "key" - }, - { - "name": "sort" - }, - { - "name": "sync" - }, - { - "name": "add_box" - }, - { - "name": "block" - }, - { - "name": "restart_alt" - }, - { - "name": "menu_open" - }, - { - "name": "shopping_cart_checkout" - }, - { - "name": "expand_circle_down" - }, - { - "name": "backspace" - }, - { - "name": "undo" - }, - { - "name": "done_all" - }, - { - "name": "do_not_disturb_on" - }, - { - "name": "open_in_full" - }, - { - "name": "double_arrow" - }, - { - "name": "sync_alt" - }, - { - "name": "zoom_in" - }, - { - "name": "done_outline" - }, - { - "name": "drag_indicator" - }, - { - "name": "fullscreen" - }, - { - "name": "star_half" - }, - { - "name": "settings_accessibility" - }, - { - "name": "reply" - }, - { - "name": "exit_to_app" - }, - { - "name": "unfold_more" - }, - { - "name": "library_add" - }, - { - "name": "cached" - }, - { - "name": "select_check_box" - }, - { - "name": "terminal" - }, - { - "name": "change_circle" - }, - { - "name": "disabled_by_default" - }, - { - "name": "swap_horiz" - }, - { - "name": "swap_vert" - }, - { - "name": "app_registration" - }, - { - "name": "download_for_offline" - }, - { - "name": "close_fullscreen" - }, - { - "name": "file_open" - }, - { - "name": "minimize" - }, - { - "name": "open_with" - }, - { - "name": "dataset" - }, - { - "name": "add_task" - }, - { - "name": "start" - }, - { - "name": "keyboard_voice" - }, - { - "name": "create_new_folder" - }, - { - "name": "forward" - }, - { - "name": "download" - }, - { - "name": "settings_applications" - }, - { - "name": "compare_arrows" - }, - { - "name": "redo" - }, - { - "name": "zoom_out" - }, - { - "name": "publish" - }, - { - "name": "html" - }, - { - "name": "token" - }, - { - "name": "switch_access_shortcut" - }, - { - "name": "fullscreen_exit" - }, - { - "name": "sort_by_alpha" - }, - { - "name": "delete_sweep" - }, - { - "name": "indeterminate_check_box" - }, - { - "name": "view_timeline" - }, - { - "name": "settings_backup_restore" - }, - { - "name": "arrow_drop_down_circle" - }, - { - "name": "assistant_navigation" - }, - { - "name": "sync_problem" - }, - { - "name": "clear_all" - }, - { - "name": "density_medium" - }, - { - "name": "heart_plus" - }, - { - "name": "filter_alt_off" - }, - { - "name": "expand" - }, - { - "name": "subdirectory_arrow_right" - }, - { - "name": "download_done" - }, - { - "name": "arrow_outward" - }, - { - "name": "123" - }, - { - "name": "swipe_left" - }, - { - "name": "auto_mode" - }, - { - "name": "saved_search" - }, - { - "name": "place_item" - }, - { - "name": "system_update_alt" - }, - { - "name": "javascript" - }, - { - "name": "search_off" - }, - { - "name": "output" - }, - { - "name": "select_all" - }, - { - "name": "fit_screen" - }, - { - "name": "swipe_up" - }, - { - "name": "dynamic_form" - }, - { - "name": "hide_source" - }, - { - "name": "swipe_right" - }, - { - "name": "switch_access_shortcut_add" - }, - { - "name": "browse_gallery" - }, - { - "name": "css" - }, - { - "name": "density_small" - }, - { - "name": "assistant_direction" - }, - { - "name": "check_small" - }, - { - "name": "youtube_searched_for" - }, - { - "name": "move_up" - }, - { - "name": "swap_horizontal_circle" - }, - { - "name": "data_thresholding" - }, - { - "name": "install_mobile" - }, - { - "name": "move_down" - }, - { - "name": "dataset_linked" - }, - { - "name": "keyboard_command_key" - }, - { - "name": "view_kanban" - }, - { - "name": "swipe_down" - }, - { - "name": "key_off" - }, - { - "name": "transcribe" - }, - { - "name": "send_time_extension" - }, - { - "name": "swipe_down_alt" - }, - { - "name": "swipe_left_alt" - }, - { - "name": "swipe_right_alt" - }, - { - "name": "swipe_up_alt" - }, - { - "name": "keyboard_option_key" - }, - { - "name": "cycle" - }, - { - "name": "rebase" - }, - { - "name": "rebase_edit" - }, - { - "name": "empty_dashboard" - }, - { - "name": "magic_exchange" - }, - { - "name": "acute" - }, - { - "name": "point_scan" - }, - { - "name": "step_into" - }, - { - "name": "cheer" - }, - { - "name": "emoticon" - }, - { - "name": "explosion" - }, - { - "name": "water_bottle" - }, - { - "name": "weather_hail" - }, - { - "name": "syringe" - }, - { - "name": "pill" - }, - { - "name": "genetics" - }, - { - "name": "allergy" - }, - { - "name": "medical_mask" - }, - { - "name": "body_fat" - }, - { - "name": "barefoot" - }, - { - "name": "infrared" - }, - { - "name": "wrist" - }, - { - "name": "metabolism" - }, - { - "name": "conditions" - }, - { - "name": "taunt" - }, - { - "name": "altitude" - }, - { - "name": "tibia" - }, - { - "name": "footprint" - }, - { - "name": "eyeglasses" - }, - { - "name": "man_3" - }, - { - "name": "woman_2" - }, - { - "name": "rheumatology" - }, - { - "name": "tornado" - }, - { - "name": "landslide" - }, - { - "name": "foggy" - }, - { - "name": "severe_cold" - }, - { - "name": "tsunami" - }, - { - "name": "vape_free" - }, - { - "name": "sign_language" - }, - { - "name": "emoji_symbols" - }, - { - "name": "clear_night" - }, - { - "name": "emoji_food_beverage" - }, - { - "name": "hive" - }, - { - "name": "thunderstorm" - }, - { - "name": "communication" - }, - { - "name": "rocket" - }, - { - "name": "pets" - }, - { - "name": "public" - }, - { - "name": "quiz" - }, - { - "name": "mood" - }, - { - "name": "gavel" - }, - { - "name": "eco" - }, - { - "name": "diamond" - }, - { - "name": "forest" - }, - { - "name": "rainy" - }, - { - "name": "skull" - } - ] -} diff --git a/web/components/emoji-icon-picker/index.tsx b/web/components/emoji-icon-picker/index.tsx deleted file mode 100644 index b9211a718..000000000 --- a/web/components/emoji-icon-picker/index.tsx +++ /dev/null @@ -1,204 +0,0 @@ -import React, { useEffect, useState, useRef } from "react"; -// headless ui -import { TwitterPicker } from "react-color"; -import { Tab, Transition, Popover } from "@headlessui/react"; -// react colors -// hooks -import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper"; -import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -// types -// emojis -import emojis from "./emojis.json"; -import { getRecentEmojis, saveRecentEmoji } from "./helpers"; -import icons from "./icons.json"; -// helpers -import { Props } from "./types"; - -const tabOptions = [ - { - key: "emoji", - title: "Emoji", - }, - { - key: "icon", - title: "Icon", - }, -]; - -const EmojiIconPicker: React.FC = (props) => { - const { label, value, onChange, onIconColorChange, disabled = false } = props; - // states - const [isOpen, setIsOpen] = useState(false); - const [openColorPicker, setOpenColorPicker] = useState(false); - const [activeColor, setActiveColor] = useState("rgb(var(--color-text-200))"); - const [recentEmojis, setRecentEmojis] = useState([]); - - const buttonRef = useRef(null); - const emojiPickerRef = useRef(null); - - useEffect(() => { - setRecentEmojis(getRecentEmojis()); - }, []); - - useEffect(() => { - if (!value || value?.length === 0) onChange(getRandomEmoji()); - }, [value, onChange]); - - useOutsideClickDetector(emojiPickerRef, () => setIsOpen(false)); - useDynamicDropdownPosition(isOpen, () => setIsOpen(false), buttonRef, emojiPickerRef); - - return ( - - setIsOpen((prev) => !prev)} - className="outline-none flex items-center justify-center" - disabled={disabled} - > - {label} - - - -
- - - {tabOptions.map((tab) => ( - - {({ selected }) => ( - - )} - - ))} - - - - {recentEmojis.length > 0 && ( -
-

Recent

-
- {recentEmojis.map((emoji) => ( - - ))} -
-
- )} -
-
-
- {emojis.map((emoji) => ( - - ))} -
-
-
-
- -
-
- {["#FF6B00", "#8CC1FF", "#FCBE1D", "#18904F", "#ADF672", "#05C3FF", "#000000"].map((curCol) => ( - setActiveColor(curCol)} - /> - ))} - -
-
- { - setActiveColor(color.hex); - if (onIconColorChange) onIconColorChange(color.hex); - }} - triangle="hide" - width="205px" - /> -
-
-
-
- {icons.material_rounded.map((icon, index) => ( - - ))} -
-
-
-
-
-
-
-
-
- ); -}; - -export default EmojiIconPicker; diff --git a/web/components/emoji-icon-picker/types.d.ts b/web/components/emoji-icon-picker/types.d.ts deleted file mode 100644 index 8a0b54342..000000000 --- a/web/components/emoji-icon-picker/types.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -export type Props = { - label: React.ReactNode; - value: any; - onChange: ( - data: - | string - | { - name: string; - color: string; - } - ) => void; - onIconColorChange?: (data: any) => void; - disabled?: boolean; - tabIndex?: number; -}; diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index 18d0543c0..468900110 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -13,7 +13,6 @@ import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelect import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; import { cn } from "helpers/common.helper"; -import { renderEmoji } from "helpers/emoji.helper"; import { truncateText } from "helpers/string.helper"; import { useApplication, @@ -33,6 +32,7 @@ import useLocalStorage from "hooks/use-local-storage"; // helpers // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; +import { ProjectLogo } from "components/project"; // constants const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => { @@ -163,13 +163,9 @@ export const CycleIssuesHeader: React.FC = observer(() => { label={currentProjectDetails?.name ?? "Project"} href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } @@ -209,7 +205,9 @@ export const CycleIssuesHeader: React.FC = observer(() => { className="ml-1.5 flex-shrink-0" placement="bottom-start" > - {currentProjectCycleIds?.map((cycleId) => )} + {currentProjectCycleIds?.map((cycleId) => ( + + ))} } /> diff --git a/web/components/headers/cycles.tsx b/web/components/headers/cycles.tsx index a0ab19ec7..22637147f 100644 --- a/web/components/headers/cycles.tsx +++ b/web/components/headers/cycles.tsx @@ -11,14 +11,15 @@ import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { CYCLE_VIEW_LAYOUTS } from "constants/cycle"; import { EUserProjectRoles } from "constants/project"; -import { renderEmoji } from "helpers/emoji.helper"; import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; import { TCycleLayout } from "@plane/types"; +import { ProjectLogo } from "components/project"; export const CyclesHeader: FC = observer(() => { // router const router = useRouter(); + const { workspaceSlug } = router.query; // store hooks const { commandPalette: { toggleCreateCycleModal }, @@ -32,9 +33,6 @@ export const CyclesHeader: FC = observer(() => { const canUserCreateCycle = currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); - const { workspaceSlug } = router.query as { - workspaceSlug: string; - }; const { setValue: setCycleLayout } = useLocalStorage("cycle_layout", "list"); const handleCurrentLayout = useCallback( @@ -58,13 +56,9 @@ export const CyclesHeader: FC = observer(() => { label={currentProjectDetails?.name ?? "Project"} href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/module-issues.tsx b/web/components/headers/module-issues.tsx index ca3a84e3b..b42b8774a 100644 --- a/web/components/headers/module-issues.tsx +++ b/web/components/headers/module-issues.tsx @@ -13,7 +13,6 @@ import { ModuleMobileHeader } from "components/modules/module-mobile-header"; import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; import { cn } from "helpers/common.helper"; -import { renderEmoji } from "helpers/emoji.helper"; import { truncateText } from "helpers/string.helper"; import { useApplication, @@ -33,6 +32,7 @@ import useLocalStorage from "hooks/use-local-storage"; // helpers // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; +import { ProjectLogo } from "components/project"; // constants const ModuleDropdownOption: React.FC<{ moduleId: string }> = ({ moduleId }) => { @@ -64,11 +64,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => { const [analyticsModal, setAnalyticsModal] = useState(false); // router const router = useRouter(); - const { workspaceSlug, projectId, moduleId } = router.query as { - workspaceSlug: string; - projectId: string; - moduleId: string; - }; + const { workspaceSlug, projectId, moduleId } = router.query; // store hooks const { issuesFilter: { issueFilters, updateFilters }, @@ -100,7 +96,13 @@ export const ModuleIssuesHeader: React.FC = observer(() => { const handleLayoutChange = useCallback( (layout: TIssueLayouts) => { if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, moduleId); + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.DISPLAY_FILTERS, + { layout: layout }, + moduleId?.toString() + ); }, [workspaceSlug, projectId, moduleId, updateFilters] ); @@ -119,7 +121,13 @@ export const ModuleIssuesHeader: React.FC = observer(() => { else newValues.push(value); } - updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, moduleId); + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.FILTERS, + { [key]: newValues }, + moduleId?.toString() + ); }, [workspaceSlug, projectId, moduleId, issueFilters, updateFilters] ); @@ -127,7 +135,13 @@ export const ModuleIssuesHeader: React.FC = observer(() => { const handleDisplayFilters = useCallback( (updatedDisplayFilter: Partial) => { if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, moduleId); + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.DISPLAY_FILTERS, + updatedDisplayFilter, + moduleId?.toString() + ); }, [workspaceSlug, projectId, moduleId, updateFilters] ); @@ -135,7 +149,13 @@ export const ModuleIssuesHeader: React.FC = observer(() => { const handleDisplayProperties = useCallback( (property: Partial) => { if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, moduleId); + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.DISPLAY_PROPERTIES, + property, + moduleId?.toString() + ); }, [workspaceSlug, projectId, moduleId, updateFilters] ); @@ -166,13 +186,9 @@ export const ModuleIssuesHeader: React.FC = observer(() => { label={currentProjectDetails?.name ?? "Project"} href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } @@ -212,7 +228,9 @@ export const ModuleIssuesHeader: React.FC = observer(() => { className="ml-1.5 flex-shrink-0" placement="bottom-start" > - {projectModuleIds?.map((moduleId) => )} + {projectModuleIds?.map((moduleId) => ( + + ))} } /> diff --git a/web/components/headers/modules-list.tsx b/web/components/headers/modules-list.tsx index b942b7b13..a1233ae52 100644 --- a/web/components/headers/modules-list.tsx +++ b/web/components/headers/modules-list.tsx @@ -10,11 +10,10 @@ import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-ham // constants import { MODULE_VIEW_LAYOUTS } from "constants/module"; import { EUserProjectRoles } from "constants/project"; -// helper -import { renderEmoji } from "helpers/emoji.helper"; // hooks import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; +import { ProjectLogo } from "components/project"; export const ModulesListHeader: React.FC = observer(() => { // router @@ -46,13 +45,9 @@ export const ModulesListHeader: React.FC = observer(() => { href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/page-details.tsx b/web/components/headers/page-details.tsx index 0eed72178..2c05d95fa 100644 --- a/web/components/headers/page-details.tsx +++ b/web/components/headers/page-details.tsx @@ -8,9 +8,9 @@ import { Breadcrumbs, Button } from "@plane/ui"; // helpers import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { renderEmoji } from "helpers/emoji.helper"; // components import { useApplication, usePage, useProject } from "hooks/store"; +import { ProjectLogo } from "components/project"; export interface IPagesHeaderProps { showButton?: boolean; @@ -42,13 +42,9 @@ export const PageDetailsHeader: FC = observer((props) => { href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/pages.tsx b/web/components/headers/pages.tsx index b5ce74fc5..e45d1a9fe 100644 --- a/web/components/headers/pages.tsx +++ b/web/components/headers/pages.tsx @@ -8,10 +8,10 @@ import { Breadcrumbs, Button } from "@plane/ui"; import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { EUserProjectRoles } from "constants/project"; -import { renderEmoji } from "helpers/emoji.helper"; // constants // components import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; +import { ProjectLogo } from "components/project"; export const PagesHeader = observer(() => { // router @@ -43,13 +43,9 @@ export const PagesHeader = observer(() => { href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/project-archived-issue-details.tsx b/web/components/headers/project-archived-issue-details.tsx index 8752e7396..86dae643d 100644 --- a/web/components/headers/project-archived-issue-details.tsx +++ b/web/components/headers/project-archived-issue-details.tsx @@ -7,8 +7,9 @@ import { Breadcrumbs, LayersIcon } from "@plane/ui"; import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { ISSUE_DETAILS } from "constants/fetch-keys"; -import { renderEmoji } from "helpers/emoji.helper"; import { useProject } from "hooks/store"; +// components +import { ProjectLogo } from "components/project"; // ui // types import { IssueArchiveService } from "services/issue"; @@ -52,13 +53,9 @@ export const ProjectArchivedIssueDetailsHeader: FC = observer(() => { href={`/${workspaceSlug}/projects`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/project-archived-issues.tsx b/web/components/headers/project-archived-issues.tsx index 8ade61aae..db208aa21 100644 --- a/web/components/headers/project-archived-issues.tsx +++ b/web/components/headers/project-archived-issues.tsx @@ -12,10 +12,10 @@ import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-ham import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues"; import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; import { useIssues, useLabel, useMember, useProject, useProjectState } from "hooks/store"; // types import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; +import { ProjectLogo } from "components/project"; export const ProjectArchivedIssuesHeader: FC = observer(() => { // router @@ -91,13 +91,9 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => { href={`/${workspaceSlug}/projects`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/project-draft-issues.tsx b/web/components/headers/project-draft-issues.tsx index 3fd0cb399..4f2929621 100644 --- a/web/components/headers/project-draft-issues.tsx +++ b/web/components/headers/project-draft-issues.tsx @@ -10,9 +10,9 @@ import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelect // ui // helper import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; -import { renderEmoji } from "helpers/emoji.helper"; import { useIssues, useLabel, useMember, useProject, useProjectState } from "hooks/store"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; +import { ProjectLogo } from "components/project"; export const ProjectDraftIssueHeader: FC = observer(() => { // router @@ -86,13 +86,9 @@ export const ProjectDraftIssueHeader: FC = observer(() => { href={`/${workspaceSlug}/projects`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/project-inbox.tsx b/web/components/headers/project-inbox.tsx index b89fbaaac..0e1bdcd1e 100644 --- a/web/components/headers/project-inbox.tsx +++ b/web/components/headers/project-inbox.tsx @@ -10,8 +10,8 @@ import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { CreateInboxIssueModal } from "components/inbox"; // helper -import { renderEmoji } from "helpers/emoji.helper"; import { useProject } from "hooks/store"; +import { ProjectLogo } from "components/project"; export const ProjectInboxHeader: FC = observer(() => { // states @@ -35,13 +35,9 @@ export const ProjectInboxHeader: FC = observer(() => { href={`/${workspaceSlug}/projects`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/project-issue-details.tsx b/web/components/headers/project-issue-details.tsx index 2f6349e61..b9343a15c 100644 --- a/web/components/headers/project-issue-details.tsx +++ b/web/components/headers/project-issue-details.tsx @@ -9,12 +9,12 @@ import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { ISSUE_DETAILS } from "constants/fetch-keys"; import { cn } from "helpers/common.helper"; -import { renderEmoji } from "helpers/emoji.helper"; import { useApplication, useProject } from "hooks/store"; // ui // helpers // services import { IssueService } from "services/issue"; +import { ProjectLogo } from "components/project"; // constants // components @@ -51,13 +51,9 @@ export const ProjectIssueDetailsHeader: FC = observer(() => { href={`/${workspaceSlug}/projects`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx index 8e8807fdb..19eaf4f4f 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/components/headers/project-issues.tsx @@ -11,7 +11,6 @@ import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelect import { IssuesMobileHeader } from "components/issues/issues-mobile-header"; import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; -import { renderEmoji } from "helpers/emoji.helper"; import { useApplication, useEventTracker, @@ -21,11 +20,12 @@ import { useUser, useMember, } from "hooks/store"; +import { useIssues } from "hooks/store/use-issues"; // components // ui // types -import { useIssues } from "hooks/store/use-issues"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; +import { ProjectLogo } from "components/project"; // constants // helper @@ -123,17 +123,9 @@ export const ProjectIssuesHeader: React.FC = observer(() => { label={currentProjectDetails?.name ?? "Project"} icon={ currentProjectDetails ? ( - currentProjectDetails?.emoji ? ( - - {renderEmoji(currentProjectDetails.emoji)} - - ) : currentProjectDetails?.icon_prop ? ( -
- {renderEmoji(currentProjectDetails.icon_prop)} -
- ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) ) : ( diff --git a/web/components/headers/project-settings.tsx b/web/components/headers/project-settings.tsx index 87b2e507e..817d842b4 100644 --- a/web/components/headers/project-settings.tsx +++ b/web/components/headers/project-settings.tsx @@ -7,9 +7,9 @@ import { Breadcrumbs, CustomMenu } from "@plane/ui"; import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "constants/project"; -import { renderEmoji } from "helpers/emoji.helper"; // hooks import { useProject, useUser } from "hooks/store"; +import { ProjectLogo } from "components/project"; // constants // components @@ -44,13 +44,9 @@ export const ProjectSettingHeader: FC = observer((props) href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/project-view-issues.tsx b/web/components/headers/project-view-issues.tsx index eea211431..4abc3edf9 100644 --- a/web/components/headers/project-view-issues.tsx +++ b/web/components/headers/project-view-issues.tsx @@ -15,7 +15,6 @@ import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelect // constants import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; -import { renderEmoji } from "helpers/emoji.helper"; import { truncateText } from "helpers/string.helper"; import { useApplication, @@ -29,6 +28,7 @@ import { useUser, } from "hooks/store"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; +import { ProjectLogo } from "components/project"; export const ProjectViewIssuesHeader: React.FC = observer(() => { // router @@ -119,17 +119,9 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - - {renderEmoji(currentProjectDetails.emoji)} - - ) : currentProjectDetails?.icon_prop ? ( -
- {renderEmoji(currentProjectDetails.icon_prop)} -
- ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/project-views.tsx b/web/components/headers/project-views.tsx index 3b4d7fb20..99533189a 100644 --- a/web/components/headers/project-views.tsx +++ b/web/components/headers/project-views.tsx @@ -8,9 +8,9 @@ import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; // helpers import { EUserProjectRoles } from "constants/project"; -import { renderEmoji } from "helpers/emoji.helper"; // constants import { useApplication, useProject, useUser } from "hooks/store"; +import { ProjectLogo } from "components/project"; export const ProjectViewsHeader: React.FC = observer(() => { // router @@ -42,17 +42,9 @@ export const ProjectViewsHeader: React.FC = observer(() => { href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - - {renderEmoji(currentProjectDetails.emoji)} - - ) : currentProjectDetails?.icon_prop ? ( -
- {renderEmoji(currentProjectDetails.icon_prop)} -
- ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/issues/issue-layouts/filters/applied-filters/project.tsx b/web/components/issues/issue-layouts/filters/applied-filters/project.tsx index 24e8fd338..84e81b6e8 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/project.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/project.tsx @@ -1,9 +1,9 @@ import { observer } from "mobx-react-lite"; import { X } from "lucide-react"; // hooks -import { renderEmoji } from "helpers/emoji.helper"; import { useProject } from "hooks/store"; -// helpers +// components +import { ProjectLogo } from "components/project"; type Props = { handleRemove: (val: string) => void; @@ -25,15 +25,9 @@ export const AppliedProjectFilters: React.FC = observer((props) => { return (
- {projectDetails.emoji ? ( - {renderEmoji(projectDetails.emoji)} - ) : projectDetails.icon_prop ? ( -
{renderEmoji(projectDetails.icon_prop)}
- ) : ( - - {projectDetails?.name.charAt(0)} - - )} + + + {projectDetails.name} {editable && (
{userProjectsData.project_data.map((project, index) => { + const projectDetails = getProjectById(project.id); + const totalIssues = project.created_issues + project.assigned_issues + project.pending_issues + project.completed_issues; @@ -138,26 +142,18 @@ export const ProfileSidebar = observer(() => { ? 0 : Math.round((project.completed_issues / project.assigned_issues) * 100); + if (!projectDetails) return null; + return ( {({ open }) => (
- {project.emoji ? ( -
- {renderEmoji(project.emoji)} -
- ) : project.icon_prop ? ( -
- {renderEmoji(project.icon_prop)} -
- ) : ( -
- {project?.name.charAt(0)} -
- )} -
{project.name}
+ + + +
{projectDetails.name}
{project.assigned_issues > 0 && ( diff --git a/web/components/project/card.tsx b/web/components/project/card.tsx index c74e9ee75..08aec43fa 100644 --- a/web/components/project/card.tsx +++ b/web/components/project/card.tsx @@ -6,14 +6,14 @@ import { LinkIcon, Lock, Pencil, Star } from "lucide-react"; // ui import { Avatar, AvatarGroup, Button, Tooltip, TOAST_TYPE, setToast, setPromiseToast } from "@plane/ui"; // components -import { DeleteProjectModal, JoinProjectModal, EUserProjectRoles } from "components/project"; +import { DeleteProjectModal, JoinProjectModal, ProjectLogo } from "components/project"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; import { copyTextToClipboard } from "helpers/string.helper"; // hooks import { useProject } from "hooks/store"; // types import type { IProject } from "@plane/types"; +import { EUserProjectRoles } from "constants/project"; // constants export type ProjectCardProps = { @@ -123,13 +123,9 @@ export const ProjectCard: React.FC = observer((props) => {
-
- - {project.emoji - ? renderEmoji(project.emoji) - : project.icon_prop - ? renderEmoji(project.icon_prop) - : null} +
+ +
diff --git a/web/components/project/create-project-modal.tsx b/web/components/project/create-project-modal.tsx index 01cbb5888..7a66c3a30 100644 --- a/web/components/project/create-project-modal.tsx +++ b/web/components/project/create-project-modal.tsx @@ -4,19 +4,30 @@ import { useForm, Controller } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; import { X } from "lucide-react"; // ui -import { Button, CustomSelect, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; +import { + Button, + CustomEmojiIconPicker, + CustomSelect, + EmojiIconPickerTypes, + Input, + setToast, + TextArea, + TOAST_TYPE, +} from "@plane/ui"; // components import { ImagePickerPopover } from "components/core"; import { MemberDropdown } from "components/dropdowns"; -import EmojiIconPicker from "components/emoji-icon-picker"; // constants import { PROJECT_CREATED } from "constants/event-tracker"; import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "constants/project"; import { EUserWorkspaceRoles } from "constants/workspace"; // helpers -import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper"; +import { convertHexEmojiToDecimal, getRandomEmoji } from "helpers/emoji.helper"; // hooks import { useEventTracker, useProject, useUser } from "hooks/store"; +import { projectIdentifierSanitizer } from "helpers/project.helper"; +import { ProjectLogo } from "./project-logo"; +import { IProject } from "@plane/types"; type Props = { isOpen: boolean; @@ -29,6 +40,21 @@ interface IIsGuestCondition { onClose: () => void; } +const defaultValues: Partial = { + cover_image: PROJECT_UNSPLASH_COVERS[Math.floor(Math.random() * PROJECT_UNSPLASH_COVERS.length)], + description: "", + logo_props: { + in_use: "emoji", + emoji: { + value: getRandomEmoji(), + }, + }, + identifier: "", + name: "", + network: 2, + project_lead: null, +}; + const IsGuestCondition: FC = ({ onClose }) => { useEffect(() => { onClose(); @@ -42,19 +68,6 @@ const IsGuestCondition: FC = ({ onClose }) => { return null; }; -export interface ICreateProjectForm { - name: string; - identifier: string; - description: string; - emoji_and_icon: string; - network: number; - project_lead_member: string; - project_lead: string; - cover_image: string; - icon_prop: any; - emoji: string; -} - export const CreateProjectModal: FC = observer((props) => { const { isOpen, onClose, setToFavorite = false, workspaceSlug } = props; // store @@ -66,7 +79,6 @@ export const CreateProjectModal: FC = observer((props) => { // states const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true); // form info - const cover_image = PROJECT_UNSPLASH_COVERS[Math.floor(Math.random() * PROJECT_UNSPLASH_COVERS.length)]; const { formState: { errors, isSubmitting }, handleSubmit, @@ -74,28 +86,20 @@ export const CreateProjectModal: FC = observer((props) => { control, watch, setValue, - } = useForm({ - defaultValues: { - cover_image, - description: "", - emoji_and_icon: getRandomEmoji(), - identifier: "", - name: "", - network: 2, - project_lead: undefined, - }, + } = useForm({ + defaultValues, reValidateMode: "onChange", }); - const currentNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network")); - if (currentWorkspaceRole && isOpen) if (currentWorkspaceRole < EUserWorkspaceRoles.MEMBER) return ; const handleClose = () => { onClose(); setIsChangeInIdentifierRequired(true); - reset(); + setTimeout(() => { + reset(); + }, 300); }; const handleAddToFavorites = (projectId: string) => { @@ -110,18 +114,11 @@ export const CreateProjectModal: FC = observer((props) => { }); }; - const onSubmit = async (formData: ICreateProjectForm) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { emoji_and_icon, project_lead_member, ...payload } = formData; - - if (typeof formData.emoji_and_icon === "object") payload.icon_prop = formData.emoji_and_icon; - else payload.emoji = formData.emoji_and_icon; - - payload.project_lead = formData.project_lead_member; + const onSubmit = async (formData: Partial) => { // Upper case identifier - payload.identifier = payload.identifier.toUpperCase(); + formData.identifier = formData.identifier?.toUpperCase(); - return createProject(workspaceSlug.toString(), payload) + return createProject(workspaceSlug.toString(), formData) .then((res) => { const newPayload = { ...res, @@ -151,7 +148,7 @@ export const CreateProjectModal: FC = observer((props) => { captureProjectEvent({ eventName: PROJECT_CREATED, payload: { - ...payload, + ...formData, state: "FAILED", }, }); @@ -165,13 +162,13 @@ export const CreateProjectModal: FC = observer((props) => { return; } if (e.target.value === "") setValue("identifier", ""); - else setValue("identifier", e.target.value.replace(/[^ÇŞĞIİÖÜA-Za-z0-9]/g, "").substring(0, 5)); + else setValue("identifier", projectIdentifierSanitizer(e.target.value).substring(0, 5)); onChange(e); }; const handleIdentifierChange = (onChange: any) => (e: ChangeEvent) => { const { value } = e.target; - const alphanumericValue = value.replace(/[^ÇŞĞIİÖÜA-Za-z0-9]/g, ""); + const alphanumericValue = projectIdentifierSanitizer(value); setIsChangeInIdentifierRequired(false); onChange(alphanumericValue); }; @@ -204,11 +201,11 @@ export const CreateProjectModal: FC = observer((props) => { >
- {watch("cover_image") !== null && ( + {watch("cover_image") && ( Cover Image )} @@ -218,30 +215,50 @@ export const CreateProjectModal: FC = observer((props) => {
- { - setValue("cover_image", image); - }} + ( + + )} />
( - - {value ? renderEmoji(value) : "Icon"} -
+ + + + } + onChange={(val) => { + let logoValue = {}; + + if (val.type === "emoji") + logoValue = { + value: convertHexEmojiToDecimal(val.value.unified), + url: val.value.imageUrl, + }; + else if (val.type === "icon") logoValue = val.value; + + onChange({ + in_use: val.type, + [val.type]: logoValue, + }); + }} + defaultIconColor={value.in_use === "icon" ? value.icon?.color : undefined} + defaultOpen={ + value.in_use === "emoji" ? EmojiIconPickerTypes.EMOJI : EmojiIconPickerTypes.ICON } - onChange={onChange} - value={value} - tabIndex={10} /> )} /> @@ -275,7 +292,9 @@ export const CreateProjectModal: FC = observer((props) => { /> )} /> - {errors?.name?.message} + + <>{errors?.name?.message} +
= observer((props) => { /> )} /> - {errors?.identifier?.message} + + <>{errors?.identifier?.message} +
= observer((props) => { ( -
- - {currentNetwork ? ( - <> - - {currentNetwork.label} - - ) : ( - Select Network - )} -
- } - placement="bottom-start" - noChevron - tabIndex={4} - > - {NETWORK_CHOICES.map((network) => ( - -
- -
-

{network.label}

-

{network.description}

-
+ render={({ field: { onChange, value } }) => { + const currentNetwork = NETWORK_CHOICES.find((n) => n.key === value); + + return ( +
+ + {currentNetwork ? ( + <> + + {currentNetwork.label} + + ) : ( + Select network + )}
- - ))} - -
- )} + } + placement="bottom-start" + noChevron + tabIndex={4} + > + {NETWORK_CHOICES.map((network) => ( + +
+ +
+

{network.label}

+

{network.description}

+
+
+
+ ))} + +
+ ); + }} /> ( -
- -
- )} + render={({ field: { value, onChange } }) => { + if (value === undefined || value === null || typeof value === "string") + return ( +
+ +
+ ); + else return <>; + }} />
@@ -396,7 +425,7 @@ export const CreateProjectModal: FC = observer((props) => { Cancel
diff --git a/web/components/project/form.tsx b/web/components/project/form.tsx index 25186e08e..1ef7ee226 100644 --- a/web/components/project/form.tsx +++ b/web/components/project/form.tsx @@ -3,22 +3,31 @@ import { Controller, useForm } from "react-hook-form"; // icons import { Lock } from "lucide-react"; // ui -import { Button, CustomSelect, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; +import { + Button, + CustomSelect, + Input, + TextArea, + TOAST_TYPE, + setToast, + CustomEmojiIconPicker, + EmojiIconPickerTypes, +} from "@plane/ui"; // components import { ImagePickerPopover } from "components/core"; -import EmojiIconPicker from "components/emoji-icon-picker"; // constants import { PROJECT_UPDATED } from "constants/event-tracker"; import { NETWORK_CHOICES } from "constants/project"; // helpers import { renderFormattedDate } from "helpers/date-time.helper"; -import { renderEmoji } from "helpers/emoji.helper"; // hooks import { useEventTracker, useProject } from "hooks/store"; // services import { ProjectService } from "services/project"; // types import { IProject, IWorkspace } from "@plane/types"; +import { ProjectLogo } from "./project-logo"; +import { convertHexEmojiToDecimal } from "helpers/emoji.helper"; export interface IProjectDetailsForm { project: IProject; workspaceSlug: string; @@ -46,7 +55,6 @@ export const ProjectDetailsForm: FC = (props) => { } = useForm({ defaultValues: { ...project, - emoji_and_icon: project.emoji ?? project.icon_prop, workspace: (project.workspace as IWorkspace).id, }, }); @@ -55,7 +63,6 @@ export const ProjectDetailsForm: FC = (props) => { if (project && projectId !== getValues("id")) { reset({ ...project, - emoji_and_icon: project.emoji ?? project.icon_prop, workspace: (project.workspace as IWorkspace).id, }); } @@ -109,14 +116,9 @@ export const ProjectDetailsForm: FC = (props) => { identifier: formData.identifier, description: formData.description, cover_image: formData.cover_image, + logo_props: formData.logo_props, }; - if (typeof formData.emoji_and_icon === "object") { - payload.emoji = null; - payload.icon_prop = formData.emoji_and_icon; - } else { - payload.emoji = formData.emoji_and_icon; - payload.icon_prop = null; - } + if (project.identifier !== formData.identifier) await projectService .checkProjectIdentifierAvailability(workspaceSlug as string, payload.identifier ?? "") @@ -139,20 +141,37 @@ export const ProjectDetailsForm: FC = (props) => {
-
- ( - - )} - /> -
+ ( + + + + } + onChange={(val) => { + let logoValue = {}; + + if (val.type === "emoji") + logoValue = { + value: convertHexEmojiToDecimal(val.value.unified), + url: val.value.imageUrl, + }; + else if (val.type === "icon") logoValue = val.value; + + onChange({ + in_use: val.type, + [val.type]: logoValue, + }); + }} + defaultIconColor={value.in_use === "icon" ? value.icon?.color : undefined} + defaultOpen={value.in_use === "emoji" ? EmojiIconPickerTypes.EMOJI : EmojiIconPickerTypes.ICON} + disabled={!isAdmin} + /> + )} + />
{watch("name")} diff --git a/web/components/project/index.ts b/web/components/project/index.ts index 42e310edb..27f3eda33 100644 --- a/web/components/project/index.ts +++ b/web/components/project/index.ts @@ -14,6 +14,7 @@ export * from "./sidebar-list"; export * from "./integration-card"; export * from "./member-list"; export * from "./member-list-item"; +export * from "./project-logo"; export * from "./project-settings-member-defaults"; export * from "./send-project-invitation-modal"; export * from "./confirm-project-member-remove"; diff --git a/web/components/project/project-logo.tsx b/web/components/project/project-logo.tsx new file mode 100644 index 000000000..3d5887b28 --- /dev/null +++ b/web/components/project/project-logo.tsx @@ -0,0 +1,34 @@ +// helpers +import { cn } from "helpers/common.helper"; +// types +import { TProjectLogoProps } from "@plane/types"; + +type Props = { + className?: string; + logo: TProjectLogoProps; +}; + +export const ProjectLogo: React.FC = (props) => { + const { className, logo } = props; + + if (logo.in_use === "icon" && logo.icon) + return ( + + {logo.icon.name} + + ); + + if (logo.in_use === "emoji" && logo.emoji) + return ( + + {logo.emoji.value?.split("-").map((emoji) => String.fromCodePoint(parseInt(emoji, 10)))} + + ); + + return ; +}; diff --git a/web/components/project/sidebar-list-item.tsx b/web/components/project/sidebar-list-item.tsx index c86ba0dc2..11ed01cd3 100644 --- a/web/components/project/sidebar-list-item.tsx +++ b/web/components/project/sidebar-list-item.tsx @@ -29,10 +29,9 @@ import { LayersIcon, setPromiseToast, } from "@plane/ui"; -import { LeaveProjectModal, PublishProjectModal } from "components/project"; +import { LeaveProjectModal, ProjectLogo, PublishProjectModal } from "components/project"; import { EUserProjectRoles } from "constants/project"; import { cn } from "helpers/common.helper"; -import { renderEmoji } from "helpers/emoji.helper"; import { getNumberCount } from "helpers/string.helper"; // hooks import { useApplication, useEventTracker, useInbox, useProject } from "hooks/store"; @@ -100,23 +99,21 @@ export const ProjectSidebarListItem: React.FC = observer((props) => { const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false); const [publishModalOpen, setPublishModal] = useState(false); const [isMenuActive, setIsMenuActive] = useState(false); + // refs + const actionSectionRef = useRef(null); // router const router = useRouter(); const { workspaceSlug, projectId: URLProjectId } = router.query; // derived values const project = getProjectById(projectId); - + const isCollapsed = themeStore.sidebarCollapsed; + const inboxesMap = project?.inbox_view ? getInboxesByProjectId(projectId) : undefined; + const inboxDetails = inboxesMap && inboxesMap.length > 0 ? getInboxById(inboxesMap[0]) : undefined; + // auth const isAdmin = project?.member_role === EUserProjectRoles.ADMIN; const isViewerOrGuest = project?.member_role && [EUserProjectRoles.VIEWER, EUserProjectRoles.GUEST].includes(project.member_role); - const isCollapsed = themeStore.sidebarCollapsed; - - const actionSectionRef = useRef(null); - - const inboxesMap = project?.inbox_view ? getInboxesByProjectId(projectId) : undefined; - const inboxDetails = inboxesMap && inboxesMap.length > 0 ? getInboxById(inboxesMap[0]) : undefined; - const handleAddToFavorites = () => { if (!workspaceSlug || !project) return; @@ -178,9 +175,13 @@ export const ProjectSidebarListItem: React.FC = observer((props) => { {({ open }) => ( <>
{provided && !disableDrag && ( = observer((props) => { >
} - className={`hidden flex-shrink-0 group-hover:block ${isMenuActive ? "!block" : ""}`} + className={cn("hidden flex-shrink-0 group-hover:block", { + "!block": isMenuActive, + })} buttonClassName="!text-custom-sidebar-text-400" ellipsis placement="bottom-start" diff --git a/web/helpers/emoji.helper.tsx b/web/helpers/emoji.helper.tsx index 5ff95027b..1fb746f51 100644 --- a/web/helpers/emoji.helper.tsx +++ b/web/helpers/emoji.helper.tsx @@ -51,3 +51,12 @@ export const groupReactions: (reactions: any[], key: string) => { [key: string]: return groupedReactions; }; + +export const convertHexEmojiToDecimal = (emojiUnified: string): string => { + if (!emojiUnified) return ""; + + return emojiUnified + .split("-") + .map((e) => parseInt(e, 16)) + .join("-"); +}; diff --git a/web/helpers/project.helper.ts b/web/helpers/project.helper.ts index 8d78964ee..441c14a42 100644 --- a/web/helpers/project.helper.ts +++ b/web/helpers/project.helper.ts @@ -43,3 +43,6 @@ export const orderJoinedProjects = ( return updatedSortOrder; }; + +export const projectIdentifierSanitizer = (identifier: string): string => + identifier.replace(/[^ÇŞĞIİÖÜA-Za-z0-9]/g, ""); diff --git a/web/layouts/settings-layout/project/sidebar.tsx b/web/layouts/settings-layout/project/sidebar.tsx index 8cf2befc2..628c2a854 100644 --- a/web/layouts/settings-layout/project/sidebar.tsx +++ b/web/layouts/settings-layout/project/sidebar.tsx @@ -24,8 +24,8 @@ export const ProjectSettingsSidebar = () => {
SETTINGS - {[...Array(8)].map(() => ( - + {[...Array(8)].map((index) => ( + ))}
diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index bc8230256..48cd5a80c 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -6,6 +6,7 @@ import { ThemeProvider } from "next-themes"; import "styles/globals.css"; import "styles/command-pallette.css"; import "styles/nprogress.css"; +import "styles/emoji.css"; import "styles/react-day-picker.css"; // constants import { THEMES } from "constants/themes"; diff --git a/web/styles/emoji.css b/web/styles/emoji.css new file mode 100644 index 000000000..2fe3ddab3 --- /dev/null +++ b/web/styles/emoji.css @@ -0,0 +1,52 @@ +.EmojiPickerReact { + --epr-category-navigation-button-size: 1.25rem !important; + --epr-category-label-height: 1.5rem !important; + --epr-emoji-size: 1.25rem !important; + --epr-picker-border-radius: 0.25rem !important; + --epr-horizontal-padding: 0.5rem !important; + --epr-emoji-padding: 0.5rem !important; + background-color: rgba(var(--color-background-100)) !important; +} + +.epr-main { + border: none !important; + border-radius: 0 !important; +} + +.epr-emoji-category-label { + font-size: 0.7875rem !important; + color: rgba(var(--color-text-300)) !important; + background-color: rgba(var(--color-background-100), 0.8) !important; +} + +.epr-category-nav, +.epr-header-overlay { + padding: 0.5rem !important; +} + +button.epr-emoji:hover > *, +button.epr-emoji:focus > * { + background-color: rgba(var(--color-background-80)) !important; +} + +input.epr-search { + font-size: 0.7875rem !important; + height: 2rem !important; + background: transparent !important; + border-color: rgba(var(--color-border-200)) !important; + border-radius: 0.25rem !important; +} + +input.epr-search::placeholder { + color: rgba(var(--color-text-400)) !important; +} + +button.epr-btn-clear-search:hover { + background-color: rgba(var(--color-background-80)) !important; + color: rgba(var(--color-text-300)) !important; +} + +.epr-emoji-variation-picker { + background-color: rgba(var(--color-background-100)) !important; + border-color: rgba(var(--color-border-200)) !important; +} diff --git a/yarn.lock b/yarn.lock index 9518afff6..c8cfcffd4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2722,7 +2722,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^18.2.42": +"@types/react@*", "@types/react@18.2.42", "@types/react@^18.2.42": version "18.2.42" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7" integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA== @@ -4064,6 +4064,11 @@ electron-to-chromium@^1.4.601: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz#4bddbc2c76e1e9dbf449ecd5da3d8119826ea4fb" integrity sha512-1n7zWYh8eS0L9Uy+GskE0lkBUNK83cXTVJI0pU3mGprFsbfSdAc15VTFbo+A+Bq4pwstmL30AVcEU3Fo463lNg== +emoji-picker-react@^4.5.16: + version "4.5.16" + resolved "https://registry.yarnpkg.com/emoji-picker-react/-/emoji-picker-react-4.5.16.tgz#12111f89a7fd2bd74965337d53806f4153d65dc6" + integrity sha512-RXaOH1EapmqbtRSMaHnwJWMfA6kiPipg/gN4cFOQRQKvrTQIA3K5+yUyzFuq8O7umIEtXUi1C1tf2dPvyyn44Q== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" From c08d6987d09bd0dfeeb38a91e7998712bcd065bb Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 6 Mar 2024 19:17:00 +0530 Subject: [PATCH 046/308] [WEB-658] fix: multiple toast alerts on adding existing issues (#3889) * fix: enums export in the types package * chore: remove NestedKeyOf type * fix: multiple toast alerts on adding existing issues * chore: added success toast alerts --- .../modals/existing-issues-list-modal.tsx | 6 ----- .../issue-layouts/empty-states/cycle.tsx | 23 +++++++++++++------ .../issue-layouts/empty-states/module.tsx | 7 ++++++ .../kanban/headers/group-by-card.tsx | 8 ++++++- .../list/headers/group-by-card.tsx | 8 ++++++- 5 files changed, 37 insertions(+), 15 deletions(-) diff --git a/web/components/core/modals/existing-issues-list-modal.tsx b/web/components/core/modals/existing-issues-list-modal.tsx index c064b02be..3e3c2871c 100644 --- a/web/components/core/modals/existing-issues-list-modal.tsx +++ b/web/components/core/modals/existing-issues-list-modal.tsx @@ -66,12 +66,6 @@ export const ExistingIssuesListModal: React.FC = (props) => { await handleOnSubmit(selectedIssues).finally(() => setIsSubmitting(false)); handleClose(); - - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Success", - message: `Issue${selectedIssues.length > 1 ? "s" : ""} added successfully`, - }); }; useEffect(() => { diff --git a/web/components/issues/issue-layouts/empty-states/cycle.tsx b/web/components/issues/issue-layouts/empty-states/cycle.tsx index 7b86c16a5..7f8c318c7 100644 --- a/web/components/issues/issue-layouts/empty-states/cycle.tsx +++ b/web/components/issues/issue-layouts/empty-states/cycle.tsx @@ -59,13 +59,22 @@ export const CycleEmptyState: React.FC = observer((props) => { const issueIds = data.map((i) => i.id); - await issues.addIssueToCycle(workspaceSlug.toString(), projectId, cycleId.toString(), issueIds).catch(() => { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Selected issues could not be added to the cycle. Please try again.", - }); - }); + await issues + .addIssueToCycle(workspaceSlug.toString(), projectId, cycleId.toString(), issueIds) + .then(() => + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Issues added to the cycle successfully.", + }) + ) + .catch(() => + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Selected issues could not be added to the cycle. Please try again.", + }) + ); }; const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS["no-issues"]; diff --git a/web/components/issues/issue-layouts/empty-states/module.tsx b/web/components/issues/issue-layouts/empty-states/module.tsx index c52d17af5..c17099335 100644 --- a/web/components/issues/issue-layouts/empty-states/module.tsx +++ b/web/components/issues/issue-layouts/empty-states/module.tsx @@ -59,6 +59,13 @@ export const ModuleEmptyState: React.FC = observer((props) => { const issueIds = data.map((i) => i.id); await issues .addIssuesToModule(workspaceSlug.toString(), projectId?.toString(), moduleId.toString(), issueIds) + .then(() => + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Issues added to the module successfully.", + }) + ) .catch(() => setToast({ type: TOAST_TYPE.ERROR, diff --git a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx index 41dda4b21..b3cc24f28 100644 --- a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx @@ -65,7 +65,13 @@ export const HeaderGroupByCard: FC = observer((props) => { const issues = data.map((i) => i.id); try { - addIssuesToView && addIssuesToView(issues); + await addIssuesToView?.(issues); + + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Issues added to the cycle successfully.", + }); } catch (error) { setToast({ type: TOAST_TYPE.ERROR, 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 7edf89bf1..acf26adb5 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 @@ -47,7 +47,13 @@ export const HeaderGroupByCard = observer( const issues = data.map((i) => i.id); try { - addIssuesToView && addIssuesToView(issues); + await addIssuesToView?.(issues); + + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Issues added to the cycle successfully.", + }); } catch (error) { setToast({ type: TOAST_TYPE.ERROR, From 66f2492e606c384ec919f19e30f8179b48038af8 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 6 Mar 2024 19:17:50 +0530 Subject: [PATCH 047/308] [WEB-655] chore: peek overview dropdowns keyboard navigation improvement (#3888) * fix: enums export in the types package * chore: remove NestedKeyOf type * chore: peek overview keyboard accessibility improvement --------- Co-authored-by: Aaryan Khandelwal --- web/components/issues/peek-overview/view.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/components/issues/peek-overview/view.tsx b/web/components/issues/peek-overview/view.tsx index aa7bd395f..c3ac1495a 100644 --- a/web/components/issues/peek-overview/view.tsx +++ b/web/components/issues/peek-overview/view.tsx @@ -58,7 +58,9 @@ export const IssueView: FC = observer((props) => { } }); const handleKeyDown = () => { - if (!isAnyModalOpen) { + const slashCommandDropdownElement = document.querySelector("#slash-command"); + const dropdownElement = document.activeElement?.tagName === "INPUT"; + if (!isAnyModalOpen && !slashCommandDropdownElement && !dropdownElement) { removeRoutePeekId(); const issueElement = document.getElementById(`issue-${issueId}`); if (issueElement) issueElement?.focus(); From dd579f83ee79460fd684d151379e2541e2ea3b80 Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Wed, 6 Mar 2024 19:27:04 +0530 Subject: [PATCH 048/308] chore: enabled module and cycle display properties in module and cycle issues (#3885) --- .../display-filters/display-properties.tsx | 49 ++++++---------- .../properties/all-properties.tsx | 58 +++++++++---------- 2 files changed, 45 insertions(+), 62 deletions(-) diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx index 871bf8ff5..d00848acd 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx @@ -1,6 +1,5 @@ import React from "react"; import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; // components import { ISSUE_DISPLAY_PROPERTIES } from "constants/issue"; import { IIssueDisplayProperties } from "@plane/types"; @@ -14,19 +13,10 @@ type Props = { }; export const FilterDisplayProperties: React.FC = observer((props) => { - const router = useRouter(); - const { moduleId, cycleId } = router.query; const { displayProperties, handleUpdate } = props; const [previewEnabled, setPreviewEnabled] = React.useState(true); - const handleDisplayPropertyVisibility = (key: keyof IIssueDisplayProperties): boolean => { - const visibility = true; - if (key === "modules" && moduleId) return false; - if (key === "cycle" && cycleId) return false; - return visibility; - }; - return ( <> = observer((props) => { /> {previewEnabled && (
- {ISSUE_DISPLAY_PROPERTIES.map( - (displayProperty) => - handleDisplayPropertyVisibility(displayProperty?.key) && ( - - ) - )} + {ISSUE_DISPLAY_PROPERTIES.map((displayProperty) => ( + + ))}
)} diff --git a/web/components/issues/issue-layouts/properties/all-properties.tsx b/web/components/issues/issue-layouts/properties/all-properties.tsx index 8b3e2e673..c3a6bc037 100644 --- a/web/components/issues/issue-layouts/properties/all-properties.tsx +++ b/web/components/issues/issue-layouts/properties/all-properties.tsx @@ -52,7 +52,7 @@ export const IssueProperties: React.FC = observer((props) => { const { getStateById } = useProjectState(); // router const router = useRouter(); - const { workspaceSlug, cycleId, moduleId } = router.query; + const { workspaceSlug } = router.query; const currentLayout = `${activeLayout} layout`; // derived values const stateDetails = getStateById(issue.state_id); @@ -328,38 +328,34 @@ export const IssueProperties: React.FC = observer((props) => { {/* modules */} - {moduleId === undefined && ( - -
- -
-
- )} + +
+ +
+
{/* cycles */} - {cycleId === undefined && ( - -
- -
-
- )} + +
+ +
+
{/* estimates */} {areEstimatesEnabledForCurrentProject && ( From f188c9fdc5c98ec74ead2799b5872d53deca90ee Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Wed, 6 Mar 2024 19:52:40 +0530 Subject: [PATCH 049/308] fix: build issues --- .../account/o-auth/o-auth-options.tsx | 12 ++- .../widgets/recent-collaborators.tsx | 94 ------------------- web/components/instance/ai-form.tsx | 1 + web/components/instance/email-form.tsx | 1 + web/components/instance/general-form.tsx | 1 + .../instance/github-config-form.tsx | 1 + .../instance/google-config-form.tsx | 1 + web/components/instance/image-config-form.tsx | 1 + .../instance/setup-form/sign-in-form.tsx | 2 + web/components/integration/github/root.tsx | 1 + web/components/issues/delete-issue-modal.tsx | 2 + .../calendar/base-calendar-root.tsx | 2 + .../gantt/quick-add-issue-form.tsx | 3 +- .../issue-layouts/kanban/base-kanban-root.tsx | 2 +- web/components/modules/modal.tsx | 2 +- web/components/onboarding/invite-members.tsx | 4 +- .../project/create-project-modal.tsx | 2 +- .../project/delete-project-modal.tsx | 1 + web/components/project/integration-card.tsx | 3 + .../project/send-project-invitation-modal.tsx | 3 + web/components/toast-alert/index.tsx | 61 ------------ web/components/ui/graphs/marimekko-graph.tsx | 48 ---------- web/components/ui/multi-level-dropdown.tsx | 14 +-- .../workspace/create-workspace-form.tsx | 1 + .../workspace/sidebar-quick-action.tsx | 2 +- web/constants/dashboard.ts | 2 +- .../settings-layout/profile/sidebar.tsx | 2 - web/layouts/user-profile-layout/layout.tsx | 5 +- .../profile/[userId]/index.tsx | 2 +- web/pages/create-workspace.tsx | 2 +- 30 files changed, 48 insertions(+), 230 deletions(-) delete mode 100644 web/components/dashboard/widgets/recent-collaborators.tsx delete mode 100644 web/components/toast-alert/index.tsx delete mode 100644 web/components/ui/graphs/marimekko-graph.tsx diff --git a/web/components/account/o-auth/o-auth-options.tsx b/web/components/account/o-auth/o-auth-options.tsx index 1671b94fc..39123328e 100644 --- a/web/components/account/o-auth/o-auth-options.tsx +++ b/web/components/account/o-auth/o-auth-options.tsx @@ -1,10 +1,12 @@ import { observer } from "mobx-react-lite"; -// services -import { TOAST_TYPE, setToast } from "@plane/ui"; -import { GitHubSignInButton, GoogleSignInButton } from "components/account"; -import { useApplication } from "hooks/store"; // ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components +import { GitHubSignInButton, GoogleSignInButton } from "components/account"; +// hooks +import { useApplication } from "hooks/store"; +// services +import { AuthService } from "services/auth.service"; type Props = { handleSignInRedirection: () => Promise; @@ -74,7 +76,7 @@ export const OAuthOptions: React.FC = observer((props) => {
{envConfig?.google_client_id && ( -
+
)} diff --git a/web/components/dashboard/widgets/recent-collaborators.tsx b/web/components/dashboard/widgets/recent-collaborators.tsx deleted file mode 100644 index 438f87c45..000000000 --- a/web/components/dashboard/widgets/recent-collaborators.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { useEffect } from "react"; -import { observer } from "mobx-react-lite"; -import Link from "next/link"; -// hooks -import { Avatar } from "@plane/ui"; -import { RecentCollaboratorsEmptyState, WidgetLoader, WidgetProps } from "components/dashboard/widgets"; -import { useDashboard, useMember, useUser } from "hooks/store"; -// components -// ui -// types -import { TRecentCollaboratorsWidgetResponse } from "@plane/types"; - -type CollaboratorListItemProps = { - issueCount: number; - userId: string; - workspaceSlug: string; -}; - -const WIDGET_KEY = "recent_collaborators"; - -const CollaboratorListItem: React.FC = observer((props) => { - const { issueCount, userId, workspaceSlug } = props; - // store hooks - const { currentUser } = useUser(); - const { getUserDetails } = useMember(); - // derived values - const userDetails = getUserDetails(userId); - const isCurrentUser = userId === currentUser?.id; - - if (!userDetails) return null; - - return ( - -
- -
-
- {isCurrentUser ? "You" : userDetails?.display_name} -
-

- {issueCount} active issue{issueCount > 1 ? "s" : ""} -

- - ); -}); - -export const RecentCollaboratorsWidget: React.FC = observer((props) => { - const { dashboardId, workspaceSlug } = props; - // store hooks - const { fetchWidgetStats, getWidgetStats } = useDashboard(); - const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - - useEffect(() => { - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - if (!widgetStats) return ; - - return ( -
-
-

Most active members

-

- Top eight active members in your project by last activity -

-
- {widgetStats.length > 1 ? ( -
- {widgetStats.map((user) => ( - - ))} -
- ) : ( -
- -
- )} -
- ); -}); diff --git a/web/components/instance/ai-form.tsx b/web/components/instance/ai-form.tsx index 63246ff34..250feb511 100644 --- a/web/components/instance/ai-form.tsx +++ b/web/components/instance/ai-form.tsx @@ -4,6 +4,7 @@ import { Eye, EyeOff } from "lucide-react"; // ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types +import { IFormattedInstanceConfiguration } from "@plane/types"; // hooks import { useApplication } from "hooks/store"; diff --git a/web/components/instance/email-form.tsx b/web/components/instance/email-form.tsx index 0caf825b6..664b96ea2 100644 --- a/web/components/instance/email-form.tsx +++ b/web/components/instance/email-form.tsx @@ -4,6 +4,7 @@ import { Controller, useForm } from "react-hook-form"; import { Eye, EyeOff } from "lucide-react"; import { Button, Input, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; // types +import { IFormattedInstanceConfiguration } from "@plane/types"; // hooks import { useApplication } from "hooks/store"; diff --git a/web/components/instance/general-form.tsx b/web/components/instance/general-form.tsx index cef757ccf..6fedc8831 100644 --- a/web/components/instance/general-form.tsx +++ b/web/components/instance/general-form.tsx @@ -3,6 +3,7 @@ import { Controller, useForm } from "react-hook-form"; // ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types +import { IInstance, IInstanceAdmin } from "@plane/types"; // hooks import { useApplication } from "hooks/store"; diff --git a/web/components/instance/github-config-form.tsx b/web/components/instance/github-config-form.tsx index 90c4c880e..20fb08aff 100644 --- a/web/components/instance/github-config-form.tsx +++ b/web/components/instance/github-config-form.tsx @@ -4,6 +4,7 @@ import { Copy, Eye, EyeOff } from "lucide-react"; // ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types +import { IFormattedInstanceConfiguration } from "@plane/types"; // hooks import { useApplication } from "hooks/store"; diff --git a/web/components/instance/google-config-form.tsx b/web/components/instance/google-config-form.tsx index 49dfcc01c..27d4f4300 100644 --- a/web/components/instance/google-config-form.tsx +++ b/web/components/instance/google-config-form.tsx @@ -4,6 +4,7 @@ import { Copy } from "lucide-react"; // ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types +import { IFormattedInstanceConfiguration } from "@plane/types"; // hooks import { useApplication } from "hooks/store"; diff --git a/web/components/instance/image-config-form.tsx b/web/components/instance/image-config-form.tsx index 9ab79aad0..7be2089f1 100644 --- a/web/components/instance/image-config-form.tsx +++ b/web/components/instance/image-config-form.tsx @@ -4,6 +4,7 @@ import { Eye, EyeOff } from "lucide-react"; // ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types +import { IFormattedInstanceConfiguration } from "@plane/types"; // hooks import { useApplication } from "hooks/store"; diff --git a/web/components/instance/setup-form/sign-in-form.tsx b/web/components/instance/setup-form/sign-in-form.tsx index 106f2d692..a2e71faf2 100644 --- a/web/components/instance/setup-form/sign-in-form.tsx +++ b/web/components/instance/setup-form/sign-in-form.tsx @@ -5,6 +5,8 @@ import { Eye, EyeOff, XCircle } from "lucide-react"; import { Input, Button, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; +// hooks +import { useUser } from "hooks/store"; // services import { AuthService } from "services/auth.service"; const authService = new AuthService(); diff --git a/web/components/integration/github/root.tsx b/web/components/integration/github/root.tsx index 956640de8..74f2f9c66 100644 --- a/web/components/integration/github/root.tsx +++ b/web/components/integration/github/root.tsx @@ -30,6 +30,7 @@ import { IntegrationService, GithubIntegrationService } from "services/integrati // types import { IGithubRepoCollaborator, IGithubServiceImportFormData } from "@plane/types"; // fetch-keys +import { APP_INTEGRATIONS, IMPORTER_SERVICES_LIST, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys"; export type TIntegrationSteps = "import-configure" | "import-data" | "repo-details" | "import-users" | "import-confirm"; export interface IIntegrationData { diff --git a/web/components/issues/delete-issue-modal.tsx b/web/components/issues/delete-issue-modal.tsx index ada126ccb..b6c08c14b 100644 --- a/web/components/issues/delete-issue-modal.tsx +++ b/web/components/issues/delete-issue-modal.tsx @@ -5,6 +5,8 @@ import { AlertTriangle } from "lucide-react"; import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import { TIssue } from "@plane/types"; +// hooks +import { useIssues, useProject } from "hooks/store"; type Props = { isOpen: boolean; diff --git a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx index 4162afe85..2a8cbcc26 100644 --- a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx +++ b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx @@ -15,6 +15,8 @@ import { TGroupedIssues, TIssue } from "@plane/types"; import { IQuickActionProps } from "../list/list-view-types"; import { EIssueActions } from "../types"; import { handleDragDrop } from "./utils"; +import { useIssues, useUser } from "hooks/store"; +import { EUserProjectRoles } from "constants/project"; interface IBaseCalendarRoot { issueStore: IProjectIssues | IModuleIssues | ICycleIssues | IProjectViewIssues; diff --git a/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx index 94a6243e5..b2d3ac9d4 100644 --- a/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx @@ -15,6 +15,7 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector"; // ui // types import { IProject, TIssue } from "@plane/types"; +import { ISSUE_CREATED } from "constants/event-tracker"; // constants interface IInputProps { @@ -162,7 +163,7 @@ export const GanttQuickAddIssueForm: React.FC = observe ) : ( -
-
-
-
- {alert.type === "success" ? ( -
-
-

{alert.title}

- {alert.message &&

{alert.message}

} -
-
-
-
- ))} -
- ); -}; - -export default ToastAlerts; diff --git a/web/components/ui/graphs/marimekko-graph.tsx b/web/components/ui/graphs/marimekko-graph.tsx deleted file mode 100644 index c0e6eb300..000000000 --- a/web/components/ui/graphs/marimekko-graph.tsx +++ /dev/null @@ -1,48 +0,0 @@ -// nivo -import { ResponsiveMarimekko, SvgProps } from "@nivo/marimekko"; -// helpers -import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; -import { generateYAxisTickValues } from "helpers/graph.helper"; -// types -import { TGraph } from "./types"; -// constants - -type Props = { - id: string; - value: string; - customYAxisTickValues?: number[]; -}; - -export const MarimekkoGraph: React.FC, "height" | "width">> = ({ - id, - value, - customYAxisTickValues, - height = "400px", - width = "100%", - margin, - theme, - ...rest -}) => ( -
- 7 ? -45 : 0, - }} - labelTextColor={{ from: "color", modifiers: [["darker", 1.6]] }} - theme={{ ...CHARTS_THEME, ...(theme ?? {}) }} - animate - {...rest} - /> -
-); diff --git a/web/components/ui/multi-level-dropdown.tsx b/web/components/ui/multi-level-dropdown.tsx index 8bb0ebcf3..8633d1586 100644 --- a/web/components/ui/multi-level-dropdown.tsx +++ b/web/components/ui/multi-level-dropdown.tsx @@ -71,7 +71,7 @@ export const MultiLevelDropdown: React.FC = ({
{ + onClick={(e: any) => { if (option.hasChildren) { e?.stopPropagation(); e?.preventDefault(); @@ -108,12 +108,12 @@ export const MultiLevelDropdown: React.FC = ({ height === "sm" ? "max-h-28" : height === "md" - ? "max-h-44" - : height === "rg" - ? "max-h-56" - : height === "lg" - ? "max-h-80" - : "" + ? "max-h-44" + : height === "rg" + ? "max-h-56" + : height === "lg" + ? "max-h-80" + : "" }`} > {option.children ? ( diff --git a/web/components/workspace/create-workspace-form.tsx b/web/components/workspace/create-workspace-form.tsx index 822ee1347..9cbfa25a3 100644 --- a/web/components/workspace/create-workspace-form.tsx +++ b/web/components/workspace/create-workspace-form.tsx @@ -12,6 +12,7 @@ import { useEventTracker, useWorkspace } from "hooks/store"; // ui // types import { IWorkspace } from "@plane/types"; +import { WorkspaceService } from "services/workspace.service"; type Props = { onSubmit?: (res: IWorkspace) => Promise; diff --git a/web/components/workspace/sidebar-quick-action.tsx b/web/components/workspace/sidebar-quick-action.tsx index d2ce2f5b3..a378a8b3f 100644 --- a/web/components/workspace/sidebar-quick-action.tsx +++ b/web/components/workspace/sidebar-quick-action.tsx @@ -27,7 +27,7 @@ export const WorkspaceSidebarQuickAction = observer(() => { membership: { currentWorkspaceRole }, } = useUser(); - const { storedValue } = useLocalStorage>>("draftedIssue", {}); + const { storedValue, setValue } = useLocalStorage>>("draftedIssue", {}); //useState control for displaying draft issue button instead of group hover const [isDraftButtonOpen, setIsDraftButtonOpen] = useState(false); diff --git a/web/constants/dashboard.ts b/web/constants/dashboard.ts index 35599d661..3d11b4f12 100644 --- a/web/constants/dashboard.ts +++ b/web/constants/dashboard.ts @@ -11,7 +11,7 @@ import OverdueIssuesLight from "public/empty-state/dashboard/light/overdue-issue import UpcomingIssuesLight from "public/empty-state/dashboard/light/upcoming-issues.svg"; // types import { TIssuesListTypes, TStateGroups } from "@plane/types"; -import { Props } from "components/icons/types"; + // constants import { EUserWorkspaceRoles } from "./workspace"; // icons diff --git a/web/layouts/settings-layout/profile/sidebar.tsx b/web/layouts/settings-layout/profile/sidebar.tsx index caa5cd56e..1bae51d8b 100644 --- a/web/layouts/settings-layout/profile/sidebar.tsx +++ b/web/layouts/settings-layout/profile/sidebar.tsx @@ -11,9 +11,7 @@ import { useApplication, useUser, useWorkspace } from "hooks/store"; import { Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { PROFILE_ACTION_LINKS } from "constants/profile"; -import { useApplication, useUser, useWorkspace } from "hooks/store"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; -import useToast from "hooks/use-toast"; const WORKSPACE_ACTION_LINKS = [ { diff --git a/web/layouts/user-profile-layout/layout.tsx b/web/layouts/user-profile-layout/layout.tsx index 243eaed1a..fcabf5f49 100644 --- a/web/layouts/user-profile-layout/layout.tsx +++ b/web/layouts/user-profile-layout/layout.tsx @@ -1,10 +1,9 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -// hooks -import { ProfileNavbar, ProfileSidebar } from "components/profile"; -import { useUser } from "hooks/store"; // components import { ProfileNavbar, ProfileSidebar } from "components/profile"; +// hooks +import { useUser } from "hooks/store"; // constants import { EUserWorkspaceRoles } from "constants/workspace"; diff --git a/web/pages/[workspaceSlug]/profile/[userId]/index.tsx b/web/pages/[workspaceSlug]/profile/[userId]/index.tsx index eb71989ed..947da1369 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/index.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/index.tsx @@ -49,7 +49,7 @@ const ProfileOverviewPage: NextPageWithLayout = () => {
- +
diff --git a/web/pages/create-workspace.tsx b/web/pages/create-workspace.tsx index 629e4a379..add8d6673 100644 --- a/web/pages/create-workspace.tsx +++ b/web/pages/create-workspace.tsx @@ -66,7 +66,7 @@ const CreateWorkspacePage: NextPageWithLayout = observer(() => {
From 6a6ab5544ad0b1e25143bed64f781bacb9da03d1 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Wed, 6 Mar 2024 20:01:14 +0530 Subject: [PATCH 050/308] fix: build issues --- space/components/issues/peek-overview/comment/add-comment.tsx | 2 +- .../issues/peek-overview/comment/comment-detail-card.tsx | 2 +- space/components/issues/peek-overview/issue-properties.tsx | 2 +- space/components/issues/peek-overview/layout.tsx | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/space/components/issues/peek-overview/comment/add-comment.tsx b/space/components/issues/peek-overview/comment/add-comment.tsx index ef1a115d2..3dba8b29c 100644 --- a/space/components/issues/peek-overview/comment/add-comment.tsx +++ b/space/components/issues/peek-overview/comment/add-comment.tsx @@ -93,7 +93,7 @@ export const AddComment: React.FC = observer((props) => { customClassName="p-2" editorContentCustomClassNames="min-h-[35px]" debouncedUpdatesEnabled={false} - onChange={(comment_json: Object, comment_html: string) => { + onChange={(comment_json: unknown, comment_html: string) => { onChange(comment_html); }} submitButton={ diff --git a/space/components/issues/peek-overview/comment/comment-detail-card.tsx b/space/components/issues/peek-overview/comment/comment-detail-card.tsx index 7c6abe199..c3a26f83e 100644 --- a/space/components/issues/peek-overview/comment/comment-detail-card.tsx +++ b/space/components/issues/peek-overview/comment/comment-detail-card.tsx @@ -115,7 +115,7 @@ export const CommentCard: React.FC = observer((props) => { value={value} debouncedUpdatesEnabled={false} customClassName="min-h-[50px] p-3 shadow-sm" - onChange={(comment_json: Object, comment_html: string) => { + onChange={(comment_json: unknown, comment_html: string) => { onChange(comment_html); }} /> diff --git a/space/components/issues/peek-overview/issue-properties.tsx b/space/components/issues/peek-overview/issue-properties.tsx index a6dcedf08..0c327ca59 100644 --- a/space/components/issues/peek-overview/issue-properties.tsx +++ b/space/components/issues/peek-overview/issue-properties.tsx @@ -94,7 +94,7 @@ export const PeekOverviewIssueProperties: React.FC = ({ issueDetails, mod > {priority && ( - + )} {priority?.title ?? "None"} diff --git a/space/components/issues/peek-overview/layout.tsx b/space/components/issues/peek-overview/layout.tsx index 5a4144db3..7345b4b28 100644 --- a/space/components/issues/peek-overview/layout.tsx +++ b/space/components/issues/peek-overview/layout.tsx @@ -11,9 +11,8 @@ import { FullScreenPeekView, SidePeekView } from "components/issues/peek-overvie // lib import { useMobxStore } from "lib/mobx/store-provider"; -type Props = {}; -export const IssuePeekOverview: React.FC = observer(() => { +export const IssuePeekOverview: React.FC = observer(() => { // states const [isSidePeekOpen, setIsSidePeekOpen] = useState(false); const [isModalPeekOpen, setIsModalPeekOpen] = useState(false); From 4861be2773327319a3ba35c61c3d020724cdd540 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Wed, 6 Mar 2024 20:15:42 +0530 Subject: [PATCH 051/308] fix: adding missing deps --- space/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/space/package.json b/space/package.json index 7018cd241..4951d5e30 100644 --- a/space/package.json +++ b/space/package.json @@ -20,6 +20,7 @@ "@plane/document-editor": "*", "@plane/lite-text-editor": "*", "@plane/rich-text-editor": "*", + "@plane/types": "*", "@plane/ui": "*", "@sentry/nextjs": "^7.85.0", "axios": "^1.3.4", From a08f401452eaf208778cd3415a00601393542648 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 6 Mar 2024 20:16:54 +0530 Subject: [PATCH 052/308] [WEB-630] refactor: empty state (#3858) * refactor: empty state global config file added and empty state component refactor * refactor: empty state component refactor * chore: empty state refactor * chore: empty state config file updated * chore: empty state action button permission logic updated * chore: empty state config file updated * chore: cycle and module empty filter state updated * chore: empty state asset updated * chore: empty state config file updated * chore: empty state config file updated * chore: empty state component improvement * chore: empty state action button improvement * fix: merge conflict --- .../cycles/active-cycle-details.tsx | 25 +- web/components/cycles/cycles-board.tsx | 23 +- web/components/cycles/cycles-list.tsx | 25 +- web/components/empty-state/empty-state.tsx | 249 ++++---- web/components/estimates/estimates-list.tsx | 27 +- web/components/exporter/guide.tsx | 31 +- web/components/integration/guide.tsx | 31 +- .../empty-states/archived-issues.tsx | 65 +-- .../issue-layouts/empty-states/cycle.tsx | 86 +-- .../empty-states/draft-issues.tsx | 61 +- .../issue-layouts/empty-states/module.tsx | 83 +-- .../empty-states/project-issues.tsx | 80 +-- .../roots/all-issue-layout-root.tsx | 59 +- .../labels/project-setting-label-list.tsx | 33 +- web/components/modules/modules-list-view.tsx | 41 +- .../page-views/workspace-dashboard.tsx | 42 +- web/components/pages/pages-list/list-view.tsx | 42 +- .../pages/pages-list/recent-pages-list.tsx | 36 +- web/components/profile/profile-issues.tsx | 31 +- web/components/project/card-list.tsx | 39 +- web/components/views/views-list.tsx | 39 +- web/constants/empty-state.ts | 536 +++++++++++------- web/pages/[workspaceSlug]/analytics.tsx | 42 +- .../projects/[projectId]/cycles/index.tsx | 48 +- .../projects/[projectId]/pages/index.tsx | 46 +- .../[projectId]/settings/integrations.tsx | 33 +- .../[workspaceSlug]/settings/api-tokens.tsx | 32 +- .../settings/webhooks/index.tsx | 29 +- ...k-resp.webp => gantt_chart-dark-resp.webp} | Bin ...{gantt-dark.webp => gantt_chart-dark.webp} | Bin ...-resp.webp => gantt_chart-light-resp.webp} | Bin ...antt-light.webp => gantt_chart-light.webp} | Bin 32 files changed, 759 insertions(+), 1155 deletions(-) rename web/public/empty-state/module-issues/{gantt-dark-resp.webp => gantt_chart-dark-resp.webp} (100%) rename web/public/empty-state/module-issues/{gantt-dark.webp => gantt_chart-dark.webp} (100%) rename web/public/empty-state/module-issues/{gantt-light-resp.webp => gantt_chart-light-resp.webp} (100%) rename web/public/empty-state/module-issues/{gantt-light.webp => gantt_chart-light.webp} (100%) diff --git a/web/components/cycles/active-cycle-details.tsx b/web/components/cycles/active-cycle-details.tsx index d9309d4b5..a6457ab3c 100644 --- a/web/components/cycles/active-cycle-details.tsx +++ b/web/components/cycles/active-cycle-details.tsx @@ -1,10 +1,9 @@ import { MouseEvent } from "react"; import { observer } from "mobx-react-lite"; import Link from "next/link"; -import { useTheme } from "next-themes"; import useSWR from "swr"; // hooks -import { useCycle, useIssues, useMember, useProject, useUser } from "hooks/store"; +import { useCycle, useIssues, useMember, useProject } from "hooks/store"; // ui import { SingleProgressStats } from "components/core"; import { @@ -23,7 +22,7 @@ import { import ProgressChart from "components/core/sidebar/progress-chart"; import { ActiveCycleProgressStats } from "components/cycles"; import { StateDropdown } from "components/dropdowns"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; // icons import { ArrowRight, CalendarCheck, CalendarDays, Star, Target } from "lucide-react"; // helpers @@ -35,7 +34,7 @@ import { ICycle, TCycleGroups } from "@plane/types"; import { EIssuesStoreType } from "constants/issue"; import { CYCLE_ISSUES_WITH_PARAMS } from "constants/fetch-keys"; import { CYCLE_STATE_GROUPS_DETAILS } from "constants/cycle"; -import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EmptyStateType } from "constants/empty-state"; interface IActiveCycleDetails { workspaceSlug: string; @@ -45,9 +44,6 @@ interface IActiveCycleDetails { export const ActiveCycleDetails: React.FC = observer((props) => { // props const { workspaceSlug, projectId } = props; - const { resolvedTheme } = useTheme(); - // store hooks - const { currentUser } = useUser(); const { issues: { fetchActiveCycleIssues }, } = useIssues(EIssuesStoreType.CYCLE); @@ -78,11 +74,6 @@ export const ActiveCycleDetails: React.FC = observer((props : null ); - const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS["active"]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("cycle", "active", isLightMode); - if (!activeCycle && isLoading) return ( @@ -90,15 +81,7 @@ export const ActiveCycleDetails: React.FC = observer((props ); - if (!activeCycle) - return ( - - ); + if (!activeCycle) return ; const endDate = new Date(activeCycle.end_date ?? ""); const startDate = new Date(activeCycle.start_date ?? ""); diff --git a/web/components/cycles/cycles-board.tsx b/web/components/cycles/cycles-board.tsx index 00c98e57c..278d55071 100644 --- a/web/components/cycles/cycles-board.tsx +++ b/web/components/cycles/cycles-board.tsx @@ -1,13 +1,10 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; -// hooks // components import { CyclePeekOverview, CyclesBoardCard } from "components/cycles"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; // constants -import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { useUser } from "hooks/store"; +import { EMPTY_STATE_DETAILS } from "constants/empty-state"; export interface ICyclesBoard { cycleIds: string[]; @@ -19,15 +16,6 @@ export interface ICyclesBoard { export const CyclesBoard: FC = observer((props) => { const { cycleIds, filter, workspaceSlug, projectId, peekCycle } = props; - // theme - const { resolvedTheme } = useTheme(); - // store hooks - const { currentUser } = useUser(); - - const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS[filter as keyof typeof CYCLE_EMPTY_STATE_DETAILS]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("cycle", filter, isLightMode); return ( <> @@ -52,12 +40,7 @@ export const CyclesBoard: FC = observer((props) => {
) : ( - + )} ); diff --git a/web/components/cycles/cycles-list.tsx b/web/components/cycles/cycles-list.tsx index 99cf1f2b1..f6ad64f99 100644 --- a/web/components/cycles/cycles-list.tsx +++ b/web/components/cycles/cycles-list.tsx @@ -1,15 +1,12 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; -// hooks // components -import { Loader } from "@plane/ui"; import { CyclePeekOverview, CyclesListItem } from "components/cycles"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; // ui +import { Loader } from "@plane/ui"; // constants -import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { useUser } from "hooks/store"; +import { EMPTY_STATE_DETAILS } from "constants/empty-state"; export interface ICyclesList { cycleIds: string[]; @@ -20,15 +17,6 @@ export interface ICyclesList { export const CyclesList: FC = observer((props) => { const { cycleIds, filter, workspaceSlug, projectId } = props; - // theme - const { resolvedTheme } = useTheme(); - // store hooks - const { currentUser } = useUser(); - - const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS[filter as keyof typeof CYCLE_EMPTY_STATE_DETAILS]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("cycle", filter, isLightMode); return ( <> @@ -54,12 +42,7 @@ export const CyclesList: FC = observer((props) => {
) : ( - + )} ) : ( diff --git a/web/components/empty-state/empty-state.tsx b/web/components/empty-state/empty-state.tsx index 9d77a81d0..9ef216068 100644 --- a/web/components/empty-state/empty-state.tsx +++ b/web/components/empty-state/empty-state.tsx @@ -1,119 +1,150 @@ import React from "react"; +import Link from "next/link"; import Image from "next/image"; -// components -// ui -import { Button, getButtonStyling } from "@plane/ui"; -// helper -import { cn } from "helpers/common.helper"; -import { ComicBoxButton } from "./comic-box-button"; -type Props = { - title: string; - description?: string; - image: any; - primaryButton?: { - icon?: any; - text: string; - onClick: () => void; - }; - secondaryButton?: { - icon?: any; - text: string; - onClick: () => void; - }; - comicBox?: { - title: string; - description: string; - }; - size?: "sm" | "lg"; - disabled?: boolean; +import { useTheme } from "next-themes"; +// hooks +import { useUser } from "hooks/store"; +// components +import { Button, TButtonVariant } from "@plane/ui"; +import { ComicBoxButton } from "./comic-box-button"; +// constant +import { EMPTY_STATE_DETAILS, EmptyStateKeys } from "constants/empty-state"; +// helpers +import { cn } from "helpers/common.helper"; + +export type EmptyStateProps = { + type: EmptyStateKeys; + size?: "sm" | "md" | "lg"; + layout?: "widget-simple" | "screen-detailed" | "screen-simple"; + additionalPath?: string; + primaryButtonOnClick?: () => void; + primaryButtonLink?: string; + secondaryButtonOnClick?: () => void; }; -export const EmptyState: React.FC = ({ - title, - description, - image, - primaryButton, - secondaryButton, - comicBox, - size = "sm", - disabled = false, -}) => { - const emptyStateHeader = ( +export const EmptyState: React.FC = (props) => { + const { + type, + size = "lg", + layout = "screen-detailed", + additionalPath = "", + primaryButtonOnClick, + primaryButtonLink, + secondaryButtonOnClick, + } = props; + // store + const { + membership: { currentWorkspaceRole, currentProjectRole }, + } = useUser(); + // theme + const { resolvedTheme } = useTheme(); + // current empty state details + const { key, title, description, path, primaryButton, secondaryButton, accessType, access } = + EMPTY_STATE_DETAILS[type]; + // resolved empty state path + const resolvedEmptyStatePath = `${additionalPath && additionalPath !== "" ? `${path}${additionalPath}` : path}-${ + resolvedTheme === "light" ? "light" : "dark" + }.webp`; + // current access type + const currentAccessType = accessType === "workspace" ? currentWorkspaceRole : currentProjectRole; + // permission + const isEditingAllowed = currentAccessType && access && currentAccessType >= access; + const anyButton = primaryButton || secondaryButton; + + // primary button + const renderPrimaryButton = () => { + if (!primaryButton) return null; + + const commonProps = { + size: size, + variant: "primary" as TButtonVariant, + prependIcon: primaryButton.icon, + onClick: primaryButtonOnClick ? primaryButtonOnClick : undefined, + disabled: !isEditingAllowed, + }; + + if (primaryButton.comicBox) { + return ( + + ); + } else if (primaryButtonLink) { + return ( + + + + ); + } else { + return ; + } + }; + // secondary button + const renderSecondaryButton = () => { + if (!secondaryButton) return null; + + return ( + + ); + }; + + return ( <> - {description ? ( - <> -

{title}

-

{description}

- - ) : ( -

{title}

+ {layout === "screen-detailed" && ( +
+
+
+ {description ? ( + <> +

{title}

+

{description}

+ + ) : ( +

{title}

+ )} +
+ + {path && ( + {key + )} + + {anyButton && ( + <> +
+ {renderPrimaryButton()} + {renderSecondaryButton()} +
+ + )} +
+
)} ); - - const secondaryButtonElement = secondaryButton && ( - - ); - - return ( -
-
-
{emptyStateHeader}
- - {primaryButton?.text - -
- {primaryButton && ( - <> -
- {comicBox ? ( - primaryButton.onClick()} - disabled={disabled} - /> - ) : ( -
primaryButton.onClick()} - > - {primaryButton.icon} - {primaryButton.text} -
- )} -
- - )} - - {secondaryButton && secondaryButtonElement} -
-
-
- ); }; diff --git a/web/components/estimates/estimates-list.tsx b/web/components/estimates/estimates-list.tsx index 8e447d6ac..1769ba016 100644 --- a/web/components/estimates/estimates-list.tsx +++ b/web/components/estimates/estimates-list.tsx @@ -1,20 +1,19 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; // store hooks -import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "components/estimates"; -import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { orderArrayBy } from "helpers/array.helper"; -import { useEstimate, useProject, useUser } from "hooks/store"; +import { useEstimate, useProject } from "hooks/store"; // components +import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "components/estimates"; +import { EmptyState } from "components/empty-state"; // ui +import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IEstimate } from "@plane/types"; // helpers +import { orderArrayBy } from "helpers/array.helper"; // constants +import { EmptyStateType } from "constants/empty-state"; export const EstimatesList: React.FC = observer(() => { // states @@ -24,12 +23,9 @@ export const EstimatesList: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { updateProject, currentProjectDetails } = useProject(); const { projectEstimates, getProjectEstimateById } = useEstimate(); - const { currentUser } = useUser(); const editEstimate = (estimate: IEstimate) => { setEstimateFormOpen(true); @@ -55,10 +51,6 @@ export const EstimatesList: React.FC = observer(() => { }); }; - const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["estimate"]; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("project-settings", "estimates", isLightMode); - return ( <> { ) : (
- +
) ) : ( diff --git a/web/components/exporter/guide.tsx b/web/components/exporter/guide.tsx index 381b168bd..03d925b62 100644 --- a/web/components/exporter/guide.tsx +++ b/web/components/exporter/guide.tsx @@ -4,26 +4,24 @@ import { observer } from "mobx-react-lite"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import useSWR, { mutate } from "swr"; // hooks -import { MoveLeft, MoveRight, RefreshCw } from "lucide-react"; -import { Button } from "@plane/ui"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { Exporter, SingleExport } from "components/exporter"; -import { ImportExportSettingsLoader } from "components/ui"; -import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EXPORT_SERVICES_LIST } from "constants/fetch-keys"; -import { EXPORTERS_LIST } from "constants/workspace"; import { useUser } from "hooks/store"; import useUserAuth from "hooks/use-user-auth"; // services import { IntegrationService } from "services/integrations"; // components +import { Exporter, SingleExport } from "components/exporter"; +import { ImportExportSettingsLoader } from "components/ui"; +import { EmptyState } from "components/empty-state"; // ui +import { Button } from "@plane/ui"; // icons -// fetch-keys +import { MoveLeft, MoveRight, RefreshCw } from "lucide-react"; // constants +import { EXPORT_SERVICES_LIST } from "constants/fetch-keys"; +import { EXPORTERS_LIST } from "constants/workspace"; +import { EmptyStateType } from "constants/empty-state"; // services const integrationService = new IntegrationService(); @@ -36,8 +34,6 @@ const IntegrationGuide = observer(() => { // router const router = useRouter(); const { workspaceSlug, provider } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { currentUser, currentUserLoader } = useUser(); // custom hooks @@ -50,10 +46,6 @@ const IntegrationGuide = observer(() => { : null ); - const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["export"]; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("workspace-settings", "exports", isLightMode); - const handleCsvClose = () => { router.replace(`/${workspaceSlug?.toString()}/settings/exports`); }; @@ -149,12 +141,7 @@ const IntegrationGuide = observer(() => {
) : (
- +
) ) : ( diff --git a/web/components/integration/guide.tsx b/web/components/integration/guide.tsx index a75c71f1f..84d422d12 100644 --- a/web/components/integration/guide.tsx +++ b/web/components/integration/guide.tsx @@ -3,28 +3,26 @@ import { observer } from "mobx-react-lite"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import useSWR, { mutate } from "swr"; // hooks -import { RefreshCw } from "lucide-react"; -import { Button } from "@plane/ui"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { DeleteImportModal, GithubImporterRoot, JiraImporterRoot, SingleImport } from "components/integration"; -import { ImportExportSettingsLoader } from "components/ui"; -import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { IMPORTER_SERVICES_LIST } from "constants/fetch-keys"; -import { IMPORTERS_LIST } from "constants/workspace"; import { useUser } from "hooks/store"; import useUserAuth from "hooks/use-user-auth"; // services import { IntegrationService } from "services/integrations"; // components +import { ImportExportSettingsLoader } from "components/ui"; +import { DeleteImportModal, GithubImporterRoot, JiraImporterRoot, SingleImport } from "components/integration"; +import { EmptyState } from "components/empty-state"; // ui +import { Button } from "@plane/ui"; // icons +import { RefreshCw } from "lucide-react"; // types import { IImporterService } from "@plane/types"; -// fetch-keys // constants +import { IMPORTER_SERVICES_LIST } from "constants/fetch-keys"; +import { IMPORTERS_LIST } from "constants/workspace"; +import { EmptyStateType } from "constants/empty-state"; // services const integrationService = new IntegrationService(); @@ -37,8 +35,6 @@ const IntegrationGuide = observer(() => { // router const router = useRouter(); const { workspaceSlug, provider } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { currentUser, currentUserLoader } = useUser(); // custom hooks @@ -49,10 +45,6 @@ const IntegrationGuide = observer(() => { workspaceSlug ? () => integrationService.getImporterServicesList(workspaceSlug as string) : null ); - const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["import"]; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("workspace-settings", "imports", isLightMode); - const handleDeleteImport = (importService: IImporterService) => { setImportToDelete(importService); setDeleteImportModal(true); @@ -145,12 +137,7 @@ const IntegrationGuide = observer(() => {
) : (
- +
) ) : ( diff --git a/web/components/issues/issue-layouts/empty-states/archived-issues.tsx b/web/components/issues/issue-layouts/empty-states/archived-issues.tsx index 96887ed60..c9de2279c 100644 --- a/web/components/issues/issue-layouts/empty-states/archived-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/archived-issues.tsx @@ -1,49 +1,27 @@ import size from "lodash/size"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; // hooks -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state"; -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { useIssues, useUser } from "hooks/store"; +import { useIssues } from "hooks/store"; // components +import { EmptyState } from "components/empty-state"; // constants +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { EmptyStateType } from "constants/empty-state"; // types import { IIssueFilterOptions } from "@plane/types"; -interface EmptyStateProps { - title: string; - image: string; - description?: string; - comicBox?: { title: string; description: string }; - primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; - secondaryButton?: { text: string; onClick: () => void }; - size?: "lg" | "sm" | undefined; - disabled?: boolean | undefined; -} - export const ProjectArchivedEmptyState: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; // theme - const { resolvedTheme } = useTheme(); // store hooks - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); const { issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED); const userFilters = issuesFilter?.issueFilters?.filters; const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode); - const EmptyStateImagePath = getEmptyStateImagePath("archived", "empty-issues", isLightMode); - const issueFilterCount = size( Object.fromEntries( Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0) @@ -61,33 +39,20 @@ export const ProjectArchivedEmptyState: React.FC = observer(() => { }); }; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - - const emptyStateProps: EmptyStateProps = - issueFilterCount > 0 - ? { - title: EMPTY_FILTER_STATE_DETAILS["archived"].title, - image: currentLayoutEmptyStateImagePath, - secondaryButton: { - text: EMPTY_FILTER_STATE_DETAILS["archived"].secondaryButton?.text, - onClick: handleClearAllFilters, - }, - } - : { - title: EMPTY_ISSUE_STATE_DETAILS["archived"].title, - description: EMPTY_ISSUE_STATE_DETAILS["archived"].description, - image: EmptyStateImagePath, - primaryButton: { - text: EMPTY_ISSUE_STATE_DETAILS["archived"].primaryButton.text, - onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`), - }, - size: "sm", - disabled: !isEditingAllowed, - }; + const emptyStateType = + issueFilterCount > 0 ? EmptyStateType.PROJECT_ARCHIVED_EMPTY_FILTER : EmptyStateType.PROJECT_ARCHIVED_NO_ISSUES; + const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined; return (
- + 0 ? undefined : `/${workspaceSlug}/projects/${projectId}/settings/automations` + } + secondaryButtonOnClick={issueFilterCount > 0 ? handleClearAllFilters : undefined} + />
); }); diff --git a/web/components/issues/issue-layouts/empty-states/cycle.tsx b/web/components/issues/issue-layouts/empty-states/cycle.tsx index 7f8c318c7..350e4dbb4 100644 --- a/web/components/issues/issue-layouts/empty-states/cycle.tsx +++ b/web/components/issues/issue-layouts/empty-states/cycle.tsx @@ -1,21 +1,17 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; -import { PlusIcon } from "lucide-react"; // hooks +import { useApplication, useEventTracker, useIssues } from "hooks/store"; +// ui import { TOAST_TYPE, setToast } from "@plane/ui"; import { ExistingIssuesListModal } from "components/core"; -// ui -// components -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { CYCLE_EMPTY_STATE_DETAILS, EMPTY_FILTER_STATE_DETAILS } from "constants/empty-state"; -import { EIssuesStoreType } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; // components +import { EmptyState } from "components/empty-state"; // types import { ISearchIssueResponse, TIssueLayouts } from "@plane/types"; // constants +import { EIssuesStoreType } from "constants/issue"; +import { EmptyStateType } from "constants/empty-state"; type Props = { workspaceSlug: string | undefined; @@ -26,33 +22,16 @@ type Props = { isEmptyFilters?: boolean; }; -interface EmptyStateProps { - title: string; - image: string; - description?: string; - comicBox?: { title: string; description: string }; - primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; - secondaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; - size?: "lg" | "sm" | undefined; - disabled?: boolean | undefined; -} - export const CycleEmptyState: React.FC = observer((props) => { const { workspaceSlug, projectId, cycleId, activeLayout, handleClearAllFilters, isEmptyFilters = false } = props; // states const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false); - // theme - const { resolvedTheme } = useTheme(); // store hooks const { issues } = useIssues(EIssuesStoreType.CYCLE); const { commandPalette: { toggleCreateIssueModal }, } = useApplication(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentProjectRole: userRole }, - currentUser, - } = useUser(); const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => { if (!workspaceSlug || !projectId || !cycleId) return; @@ -77,43 +56,9 @@ export const CycleEmptyState: React.FC = observer((props) => { ); }; - const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS["no-issues"]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode); - const emptyStateImage = getEmptyStateImagePath("cycle-issues", activeLayout ?? "list", isLightMode); - - const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER; - - const emptyStateProps: EmptyStateProps = isEmptyFilters - ? { - title: EMPTY_FILTER_STATE_DETAILS["project"].title, - image: currentLayoutEmptyStateImagePath, - secondaryButton: { - text: EMPTY_FILTER_STATE_DETAILS["project"].secondaryButton.text, - onClick: handleClearAllFilters, - }, - } - : { - title: emptyStateDetail.title, - description: emptyStateDetail.description, - image: emptyStateImage, - primaryButton: { - text: emptyStateDetail.primaryButton.text, - icon: , - onClick: () => { - setTrackElement("Cycle issue empty state"); - toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); - }, - }, - secondaryButton: { - text: emptyStateDetail.secondaryButton.text, - icon: , - onClick: () => setCycleIssuesListModal(true), - }, - size: "sm", - disabled: !isEditingAllowed, - }; + const emptyStateType = isEmptyFilters ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_CYCLE_NO_ISSUES; + const additionalPath = activeLayout ?? "list"; + const emptyStateSize = isEmptyFilters ? "lg" : "sm"; return ( <> @@ -126,7 +71,20 @@ export const CycleEmptyState: React.FC = observer((props) => { handleOnSubmit={handleAddIssuesToCycle} />
- + { + setTrackElement("Cycle issue empty state"); + toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); + } + } + secondaryButtonOnClick={isEmptyFilters ? handleClearAllFilters : () => setCycleIssuesListModal(true)} + />
); diff --git a/web/components/issues/issue-layouts/empty-states/draft-issues.tsx b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx index 77b1123b6..0968ed07a 100644 --- a/web/components/issues/issue-layouts/empty-states/draft-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx @@ -1,49 +1,26 @@ import size from "lodash/size"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; // hooks -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state"; -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { useIssues, useUser } from "hooks/store"; +import { useIssues } from "hooks/store"; // components +import { EmptyState } from "components/empty-state"; // constants +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { EmptyStateType } from "constants/empty-state"; // types import { IIssueFilterOptions } from "@plane/types"; -interface EmptyStateProps { - title: string; - image: string; - description?: string; - comicBox?: { title: string; description: string }; - primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; - secondaryButton?: { text: string; onClick: () => void }; - size?: "lg" | "sm" | undefined; - disabled?: boolean | undefined; -} - export const ProjectDraftEmptyState: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); const { issuesFilter } = useIssues(EIssuesStoreType.DRAFT); const userFilters = issuesFilter?.issueFilters?.filters; const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode); - const EmptyStateImagePath = getEmptyStateImagePath("draft", "draft-issues-empty", isLightMode); - const issueFilterCount = size( Object.fromEntries( Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0) @@ -61,29 +38,19 @@ export const ProjectDraftEmptyState: React.FC = observer(() => { }); }; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - - const emptyStateProps: EmptyStateProps = - issueFilterCount > 0 - ? { - title: EMPTY_FILTER_STATE_DETAILS["draft"].title, - image: currentLayoutEmptyStateImagePath, - secondaryButton: { - text: EMPTY_FILTER_STATE_DETAILS["draft"].secondaryButton.text, - onClick: handleClearAllFilters, - }, - } - : { - title: EMPTY_ISSUE_STATE_DETAILS["draft"].title, - description: EMPTY_ISSUE_STATE_DETAILS["draft"].description, - image: EmptyStateImagePath, - size: "sm", - disabled: !isEditingAllowed, - }; + const emptyStateType = + issueFilterCount > 0 ? EmptyStateType.PROJECT_DRAFT_EMPTY_FILTER : EmptyStateType.PROJECT_DRAFT_NO_ISSUES; + const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined; + const emptyStateSize = issueFilterCount > 0 ? "lg" : "sm"; return (
- + 0 ? handleClearAllFilters : undefined} + />
); }); diff --git a/web/components/issues/issue-layouts/empty-states/module.tsx b/web/components/issues/issue-layouts/empty-states/module.tsx index c17099335..6c0cd0cd6 100644 --- a/web/components/issues/issue-layouts/empty-states/module.tsx +++ b/web/components/issues/issue-layouts/empty-states/module.tsx @@ -1,20 +1,18 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; -import { PlusIcon } from "lucide-react"; // hooks +import { useApplication, useEventTracker, useIssues } from "hooks/store"; +// ui import { TOAST_TYPE, setToast } from "@plane/ui"; -import { ExistingIssuesListModal } from "components/core"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { EMPTY_FILTER_STATE_DETAILS, MODULE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EIssuesStoreType } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; // ui // components +import { ExistingIssuesListModal } from "components/core"; +import { EmptyState } from "components/empty-state"; // types import { ISearchIssueResponse, TIssueLayouts } from "@plane/types"; // constants +import { EIssuesStoreType } from "constants/issue"; +import { EmptyStateType } from "constants/empty-state"; type Props = { workspaceSlug: string | undefined; @@ -25,33 +23,16 @@ type Props = { isEmptyFilters?: boolean; }; -interface EmptyStateProps { - title: string; - image: string; - description?: string; - comicBox?: { title: string; description: string }; - primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; - secondaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; - size?: "lg" | "sm" | undefined; - disabled?: boolean | undefined; -} - export const ModuleEmptyState: React.FC = observer((props) => { const { workspaceSlug, projectId, moduleId, activeLayout, handleClearAllFilters, isEmptyFilters = false } = props; // states const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false); - // theme - const { resolvedTheme } = useTheme(); // store hooks const { issues } = useIssues(EIssuesStoreType.MODULE); const { commandPalette: { toggleCreateIssueModal }, } = useApplication(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentProjectRole: userRole }, - currentUser, - } = useUser(); const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => { if (!workspaceSlug || !projectId || !moduleId) return; @@ -75,42 +56,8 @@ export const ModuleEmptyState: React.FC = observer((props) => { ); }; - const emptyStateDetail = MODULE_EMPTY_STATE_DETAILS["no-issues"]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode); - const emptyStateImage = getEmptyStateImagePath("module-issues", activeLayout ?? "list", isLightMode); - - const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER; - - const emptyStateProps: EmptyStateProps = isEmptyFilters - ? { - title: EMPTY_FILTER_STATE_DETAILS["project"].title, - image: currentLayoutEmptyStateImagePath, - secondaryButton: { - text: EMPTY_FILTER_STATE_DETAILS["project"].secondaryButton.text, - onClick: handleClearAllFilters, - }, - } - : { - title: emptyStateDetail.title, - description: emptyStateDetail.description, - image: emptyStateImage, - primaryButton: { - text: emptyStateDetail.primaryButton.text, - icon: , - onClick: () => { - setTrackElement("Module issue empty state"); - toggleCreateIssueModal(true, EIssuesStoreType.MODULE); - }, - }, - secondaryButton: { - text: emptyStateDetail.secondaryButton.text, - icon: , - onClick: () => setModuleIssuesListModal(true), - }, - disabled: !isEditingAllowed, - }; + const emptyStateType = isEmptyFilters ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_MODULE_ISSUES; + const additionalPath = activeLayout ?? "list"; return ( <> @@ -123,7 +70,19 @@ export const ModuleEmptyState: React.FC = observer((props) => { handleOnSubmit={handleAddIssuesToModule} />
- + { + setTrackElement("Cycle issue empty state"); + toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); + } + } + secondaryButtonOnClick={isEmptyFilters ? handleClearAllFilters : () => setModuleIssuesListModal(true)} + />
); diff --git a/web/components/issues/issue-layouts/empty-states/project-issues.tsx b/web/components/issues/issue-layouts/empty-states/project-issues.tsx index e44dd5626..12642d364 100644 --- a/web/components/issues/issue-layouts/empty-states/project-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/project-issues.tsx @@ -1,51 +1,29 @@ import size from "lodash/size"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; // hooks -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state"; -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; +import { useApplication, useEventTracker, useIssues } from "hooks/store"; // components +import { EmptyState } from "components/empty-state"; // constants +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { EmptyStateType } from "constants/empty-state"; // types import { IIssueFilterOptions } from "@plane/types"; -interface EmptyStateProps { - title: string; - image: string; - description?: string; - comicBox?: { title: string; description: string }; - primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; - secondaryButton?: { text: string; onClick: () => void }; - size?: "lg" | "sm" | undefined; - disabled?: boolean | undefined; -} - export const ProjectEmptyState: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: commandPaletteStore } = useApplication(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); + const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT); const userFilters = issuesFilter?.issueFilters?.filters; const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode); - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "issues", isLightMode); - const issueFilterCount = size( Object.fromEntries( Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0) @@ -63,40 +41,26 @@ export const ProjectEmptyState: React.FC = observer(() => { }); }; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - - const emptyStateProps: EmptyStateProps = - issueFilterCount > 0 - ? { - title: EMPTY_FILTER_STATE_DETAILS["project"].title, - image: currentLayoutEmptyStateImagePath, - secondaryButton: { - text: EMPTY_FILTER_STATE_DETAILS["project"].secondaryButton.text, - onClick: handleClearAllFilters, - }, - } - : { - title: EMPTY_ISSUE_STATE_DETAILS["project"].title, - description: EMPTY_ISSUE_STATE_DETAILS["project"].description, - image: EmptyStateImagePath, - comicBox: { - title: EMPTY_ISSUE_STATE_DETAILS["project"].comicBox.title, - description: EMPTY_ISSUE_STATE_DETAILS["project"].comicBox.description, - }, - primaryButton: { - text: EMPTY_ISSUE_STATE_DETAILS["project"].primaryButton.text, - onClick: () => { - setTrackElement("Project issue empty state"); - commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); - }, - }, - size: "lg", - disabled: !isEditingAllowed, - }; + const emptyStateType = issueFilterCount > 0 ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_NO_ISSUES; + const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined; + const emptyStateSize = issueFilterCount > 0 ? "lg" : "sm"; return (
- + 0 + ? undefined + : () => { + setTrackElement("Project issue empty state"); + commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); + } + } + secondaryButtonOnClick={issueFilterCount > 0 ? handleClearAllFilters : undefined} + />
); }); diff --git a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx index 84101542f..1367eccc4 100644 --- a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx @@ -2,32 +2,28 @@ import React, { Fragment, useCallback, useMemo } from "react"; import isEmpty from "lodash/isEmpty"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import useSWR from "swr"; // hooks -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { useWorkspaceIssueProperties } from "hooks/use-workspace-issue-properties"; +import { useApplication, useEventTracker, useGlobalView, useIssues, useProject, useUser } from "hooks/store"; +// components import { GlobalViewsAppliedFiltersRoot, IssuePeekOverview } from "components/issues"; import { SpreadsheetView } from "components/issues/issue-layouts"; import { AllIssueQuickActions } from "components/issues/issue-layouts/quick-action-dropdowns"; +import { EmptyState } from "components/empty-state"; import { SpreadsheetLayoutLoader } from "components/ui"; -import { ALL_ISSUES_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { useApplication, useEventTracker, useGlobalView, useIssues, useProject, useUser } from "hooks/store"; -import { useWorkspaceIssueProperties } from "hooks/use-workspace-issue-properties"; -// components // types import { TIssue, IIssueDisplayFilterOptions } from "@plane/types"; import { EIssueActions } from "../types"; // constants +import { EUserProjectRoles } from "constants/project"; +import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { EMPTY_STATE_DETAILS, EmptyStateType } from "constants/empty-state"; export const AllIssueLayoutRoot: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, globalViewId, ...routeFilters } = router.query; - // theme - const { resolvedTheme } = useTheme(); //swr hook for fetching issue properties useWorkspaceIssueProperties(workspaceSlug); // store @@ -39,8 +35,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { const { dataViewId, issueIds } = groupedIssueIds; const { - membership: { currentWorkspaceAllProjectsRole, currentWorkspaceRole }, - currentUser, + membership: { currentWorkspaceAllProjectsRole }, } = useUser(); const { fetchAllGlobalViews } = useGlobalView(); const { workspaceProjectIds } = useProject(); @@ -48,10 +43,6 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { const isDefaultView = ["all-issues", "assigned", "created", "subscribed"].includes(groupedIssueIds.dataViewId); const currentView = isDefaultView ? groupedIssueIds.dataViewId : "custom-view"; - const currentViewDetails = ALL_ISSUES_EMPTY_STATE_DETAILS[currentView as keyof typeof ALL_ISSUES_EMPTY_STATE_DETAILS]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("all-issues", currentView, isLightMode); // filter init from the query params @@ -185,46 +176,34 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { [canEditProperties, handleIssues] ); - const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; - if (loader === "init-loader" || !globalViewId || globalViewId !== dataViewId || !issueIds) { return ; } + const emptyStateType = + (workspaceProjectIds ?? []).length > 0 ? `workspace-${currentView}` : EmptyStateType.WORKSPACE_NO_PROJECTS; + return (
{issueIds.length === 0 ? ( 0 ? currentViewDetails.title : "No project"} - description={ - (workspaceProjectIds ?? []).length > 0 - ? currentViewDetails.description - : "To create issues or manage your work, you need to create a project or be a part of one." - } + type={emptyStateType as keyof typeof EMPTY_STATE_DETAILS} size="sm" - primaryButton={ + primaryButtonOnClick={ (workspaceProjectIds ?? []).length > 0 ? currentView !== "custom-view" && currentView !== "subscribed" - ? { - text: "Create new issue", - onClick: () => { - setTrackElement("All issues empty state"); - commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); - }, + ? () => { + setTrackElement("All issues empty state"); + commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); } : undefined - : { - text: "Start your first project", - onClick: () => { - setTrackElement("All issues empty state"); - commandPaletteStore.toggleCreateProjectModal(true); - }, + : () => { + setTrackElement("All issues empty state"); + commandPaletteStore.toggleCreateProjectModal(true); } } - disabled={!isEditingAllowed} /> ) : ( diff --git a/web/components/labels/project-setting-label-list.tsx b/web/components/labels/project-setting-label-list.tsx index ba6b43b0b..1e83167ae 100644 --- a/web/components/labels/project-setting-label-list.tsx +++ b/web/components/labels/project-setting-label-list.tsx @@ -1,4 +1,6 @@ import React, { useState, useRef } from "react"; +import { useRouter } from "next/router"; +import { observer } from "mobx-react"; import { DragDropContext, Draggable, @@ -7,26 +9,23 @@ import { DropResult, Droppable, } from "@hello-pangea/dnd"; -import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; // hooks -import { Button, Loader } from "@plane/ui"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { useLabel } from "hooks/store"; +import useDraggableInPortal from "hooks/use-draggable-portal"; +// components import { CreateUpdateLabelInline, DeleteLabelModal, ProjectSettingLabelGroup, ProjectSettingLabelItem, } from "components/labels"; -import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { useLabel, useUser } from "hooks/store"; -import useDraggableInPortal from "hooks/use-draggable-portal"; -// components +import { EmptyState } from "components/empty-state"; // ui +import { Button, Loader } from "@plane/ui"; // types import { IIssueLabel } from "@plane/types"; // constants +import { EmptyStateType } from "constants/empty-state"; const LABELS_ROOT = "labels.root"; @@ -41,10 +40,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks - const { currentUser } = useUser(); const { projectLabels, updateLabelPosition, projectLabelsTree } = useLabel(); // portal const renderDraggable = useDraggableInPortal(); @@ -54,10 +50,6 @@ export const ProjectSettingsLabelList: React.FC = observer(() => { setLabelForm(true); }; - const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["labels"]; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("project-settings", "labels", isLightMode); - const onDragEnd = (result: DropResult) => { const { combine, draggableId, destination, source } = result; @@ -121,13 +113,8 @@ export const ProjectSettingsLabelList: React.FC = observer(() => { )} {projectLabels ? ( projectLabels.length === 0 && !showLabelForm ? ( -
- +
+
) : ( projectLabelsTree && ( diff --git a/web/components/modules/modules-list-view.tsx b/web/components/modules/modules-list-view.tsx index 33c11cbd8..78b4a6571 100644 --- a/web/components/modules/modules-list-view.tsx +++ b/web/components/modules/modules-list-view.tsx @@ -1,40 +1,28 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; // hooks +import { useApplication, useEventTracker, useModule } from "hooks/store"; +import useLocalStorage from "hooks/use-local-storage"; // components -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "components/modules"; +import { EmptyState } from "components/empty-state"; // ui import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui"; // constants -import { MODULE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EUserProjectRoles } from "constants/project"; -import { useApplication, useEventTracker, useModule, useUser } from "hooks/store"; -import useLocalStorage from "hooks/use-local-storage"; +import { EmptyStateType } from "constants/empty-state"; export const ModulesListView: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId, peekModule } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: commandPaletteStore } = useApplication(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); + const { projectModuleIds, loader } = useModule(); const { storedValue: modulesView } = useLocalStorage("modules_view", "grid"); - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "modules", isLightMode); - - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - if (loader || !projectModuleIds) return ( <> @@ -88,22 +76,11 @@ export const ModulesListView: React.FC = observer(() => { ) : ( { + setTrackElement("Module empty state"); + commandPaletteStore.toggleCreateModuleModal(true); }} - primaryButton={{ - text: MODULE_EMPTY_STATE_DETAILS["modules"].primaryButton.text, - onClick: () => { - setTrackElement("Module empty state"); - commandPaletteStore.toggleCreateModuleModal(true); - }, - }} - size="lg" - disabled={!isEditingAllowed} /> )} diff --git a/web/components/page-views/workspace-dashboard.tsx b/web/components/page-views/workspace-dashboard.tsx index 2f8392bc2..f910625ca 100644 --- a/web/components/page-views/workspace-dashboard.tsx +++ b/web/components/page-views/workspace-dashboard.tsx @@ -1,41 +1,30 @@ import { useEffect } from "react"; import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; // hooks +import { useApplication, useEventTracker, useDashboard, useProject, useUser } from "hooks/store"; // components -import { Spinner } from "@plane/ui"; import { DashboardWidgets } from "components/dashboard"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; import { IssuePeekOverview } from "components/issues"; import { TourRoot } from "components/onboarding"; import { UserGreetingsView } from "components/user"; // ui +import { Spinner } from "@plane/ui"; // constants -import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state"; import { PRODUCT_TOUR_COMPLETED } from "constants/event-tracker"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { useApplication, useEventTracker, useDashboard, useProject, useUser } from "hooks/store"; +import { EmptyStateType } from "constants/empty-state"; export const WorkspaceDashboardView = observer(() => { - // theme - const { resolvedTheme } = useTheme(); // store hooks const { captureEvent, setTrackElement } = useEventTracker(); const { commandPalette: { toggleCreateProjectModal }, router: { workspaceSlug }, } = useApplication(); - const { - currentUser, - updateTourCompleted, - membership: { currentWorkspaceRole }, - } = useUser(); + const { currentUser, updateTourCompleted } = useUser(); const { homeDashboardId, fetchHomeDashboardWidgets } = useDashboard(); const { joinedProjectIds } = useProject(); - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("onboarding", "dashboard", isLightMode); - const handleTourCompleted = () => { updateTourCompleted() .then(() => { @@ -56,8 +45,6 @@ export const WorkspaceDashboardView = observer(() => { fetchHomeDashboardWidgets(workspaceSlug); }, [fetchHomeDashboardWidgets, workspaceSlug]); - const isEditingAllowed = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; - return ( <> {currentUser && !currentUser.is_tour_completed && ( @@ -78,22 +65,11 @@ export const WorkspaceDashboardView = observer(() => { ) : ( { - setTrackElement("Dashboard empty state"); - toggleCreateProjectModal(true); - }, + type={EmptyStateType.WORKSPACE_DASHBOARD} + primaryButtonOnClick={() => { + setTrackElement("Dashboard empty state"); + toggleCreateProjectModal(true); }} - comicBox={{ - title: WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].comicBox.title, - description: WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].comicBox.description, - }} - size="lg" - disabled={!isEditingAllowed} /> )} diff --git a/web/components/pages/pages-list/list-view.tsx b/web/components/pages/pages-list/list-view.tsx index 0d468ef3c..8c1a09e73 100644 --- a/web/components/pages/pages-list/list-view.tsx +++ b/web/components/pages/pages-list/list-view.tsx @@ -1,17 +1,15 @@ import { FC } from "react"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; // hooks -import { Loader } from "@plane/ui"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EUserProjectRoles } from "constants/project"; -import { useApplication, useUser } from "hooks/store"; +import { useApplication } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // components +import { EmptyState } from "components/empty-state"; import { PagesListItem } from "./list-item"; // ui +import { Loader } from "@plane/ui"; // constants +import { EMPTY_STATE_DETAILS, EmptyStateType } from "constants/empty-state"; type IPagesListView = { pageIds: string[]; @@ -19,34 +17,20 @@ type IPagesListView = { export const PagesListView: FC = (props) => { const { pageIds: projectPageIds } = props; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: { toggleCreatePageModal }, } = useApplication(); - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); // local storage const { storedValue: pageTab } = useLocalStorage("pageTab", "Recent"); // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const currentPageTabDetails = pageTab - ? PAGE_EMPTY_STATE_DETAILS[pageTab as keyof typeof PAGE_EMPTY_STATE_DETAILS] - : PAGE_EMPTY_STATE_DETAILS["All"]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("pages", currentPageTabDetails.key, isLightMode); - - const isButtonVisible = currentPageTabDetails.key !== "archived" && currentPageTabDetails.key !== "favorites"; - // here we are only observing the projectPageStore, so that we can re-render the component when the projectPageStore changes - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const emptyStateType = pageTab ? `project-page-${pageTab}` : EmptyStateType.PROJECT_PAGE_ALL; + const isButtonVisible = pageTab !== "archived" && pageTab !== "favorites"; return ( <> @@ -60,18 +44,8 @@ export const PagesListView: FC = (props) => { ) : ( toggleCreatePageModal(true), - } - : undefined - } - disabled={!isEditingAllowed} + type={emptyStateType as keyof typeof EMPTY_STATE_DETAILS} + primaryButtonOnClick={isButtonVisible ? () => toggleCreatePageModal(true) : undefined} /> )}
diff --git a/web/components/pages/pages-list/recent-pages-list.tsx b/web/components/pages/pages-list/recent-pages-list.tsx index 28a430031..45de8db0d 100644 --- a/web/components/pages/pages-list/recent-pages-list.tsx +++ b/web/components/pages/pages-list/recent-pages-list.tsx @@ -1,39 +1,27 @@ import React, { FC } from "react"; import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; // hooks -import { Loader } from "@plane/ui"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { PagesListView } from "components/pages/pages-list"; -import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EUserProjectRoles } from "constants/project"; -import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; -import { useApplication, useUser } from "hooks/store"; +import { useApplication } from "hooks/store"; import { useProjectPages } from "hooks/store/use-project-specific-pages"; // components +import { PagesListView } from "components/pages/pages-list"; +import { EmptyState } from "components/empty-state"; // ui +import { Loader } from "@plane/ui"; // helpers +import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; // constants +import { EmptyStateType } from "constants/empty-state"; export const RecentPagesList: FC = observer(() => { - // theme - const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: commandPaletteStore } = useApplication(); - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); - const { recentProjectPages } = useProjectPages(); - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const EmptyStateImagePath = getEmptyStateImagePath("pages", "recent", isLightMode); + const { recentProjectPages } = useProjectPages(); // FIXME: replace any with proper type const isEmpty = recentProjectPages && Object.values(recentProjectPages).every((value: any) => value.length === 0); - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - if (!recentProjectPages) { return ( @@ -64,15 +52,9 @@ export const RecentPagesList: FC = observer(() => { ) : ( <> commandPaletteStore.toggleCreatePageModal(true), - }} + type={EmptyStateType.PROJECT_PAGE_RECENT} + primaryButtonOnClick={() => commandPaletteStore.toggleCreatePageModal(true)} size="sm" - disabled={!isEditingAllowed} /> )} diff --git a/web/components/profile/profile-issues.tsx b/web/components/profile/profile-issues.tsx index b6a99baf9..f94c1d91f 100644 --- a/web/components/profile/profile-issues.tsx +++ b/web/components/profile/profile-issues.tsx @@ -1,20 +1,18 @@ import React, { useEffect } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import useSWR from "swr"; // components -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { IssuePeekOverview, ProfileIssuesAppliedFiltersRoot } from "components/issues"; import { ProfileIssuesKanBanLayout } from "components/issues/issue-layouts/kanban/roots/profile-issues-root"; import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root"; import { KanbanLayoutLoader, ListLayoutLoader } from "components/ui"; +import { EmptyState } from "components/empty-state"; // hooks -import { PROFILE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EIssuesStoreType } from "constants/issue"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { useIssues, useUser } from "hooks/store"; +import { useIssues } from "hooks/store"; // constants +import { EIssuesStoreType } from "constants/issue"; +import { EMPTY_STATE_DETAILS } from "constants/empty-state"; interface IProfileIssuesPage { type: "assigned" | "subscribed" | "created"; @@ -28,13 +26,7 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => { workspaceSlug: string; userId: string; }; - // theme - const { resolvedTheme } = useTheme(); // store hooks - const { - membership: { currentWorkspaceRole }, - currentUser, - } = useUser(); const { issues: { loader, groupedIssueIds, fetchIssues, setViewId }, issuesFilter: { issueFilters, fetchFilters }, @@ -55,26 +47,15 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => { { revalidateIfStale: false, revalidateOnFocus: false } ); - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("profile", type, isLightMode); - const activeLayout = issueFilters?.displayFilters?.layout || undefined; - const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; + const emptyStateType = `profile-${type}`; if (!groupedIssueIds || loader === "init-loader") return <>{activeLayout === "list" ? : }; if (groupedIssueIds.length === 0) { - return ( - - ); + return ; } return ( diff --git a/web/components/project/card-list.tsx b/web/components/project/card-list.tsx index a19b53fbb..df63dfb73 100644 --- a/web/components/project/card-list.tsx +++ b/web/components/project/card-list.tsx @@ -1,32 +1,20 @@ import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; // hooks +import { useApplication, useEventTracker, useProject } from "hooks/store"; // components -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; import { ProjectCard } from "components/project"; import { ProjectsLoader } from "components/ui"; // constants -import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; +import { EmptyStateType } from "constants/empty-state"; export const ProjectCardList = observer(() => { - // theme - const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: commandPaletteStore } = useApplication(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentWorkspaceRole }, - currentUser, - } = useUser(); + const { workspaceProjectIds, searchedProjects, getProjectById } = useProject(); - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("onboarding", "projects", isLightMode); - - const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; - if (!workspaceProjectIds) return ; return ( @@ -49,22 +37,11 @@ export const ProjectCardList = observer(() => {
) : ( { - setTrackElement("Project empty state"); - commandPaletteStore.toggleCreateProjectModal(true); - }, + type={EmptyStateType.WORKSPACE_PROJECTS} + primaryButtonOnClick={() => { + setTrackElement("Project empty state"); + commandPaletteStore.toggleCreateProjectModal(true); }} - comicBox={{ - title: WORKSPACE_EMPTY_STATE_DETAILS["projects"].comicBox.title, - description: WORKSPACE_EMPTY_STATE_DETAILS["projects"].comicBox.description, - }} - size="lg" - disabled={!isEditingAllowed} /> )} diff --git a/web/components/views/views-list.tsx b/web/components/views/views-list.tsx index 9d8bf85e6..ba4bef2b8 100644 --- a/web/components/views/views-list.tsx +++ b/web/components/views/views-list.tsx @@ -1,45 +1,32 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; import { Search } from "lucide-react"; // hooks +import { useApplication, useProjectView } from "hooks/store"; // components -import { Input } from "@plane/ui"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -// ui +import { EmptyState } from "components/empty-state"; import { ViewListLoader } from "components/ui"; import { ProjectViewListItem } from "components/views"; +// ui +import { Input } from "@plane/ui"; // constants -import { VIEW_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EUserProjectRoles } from "constants/project"; -import { useApplication, useProjectView, useUser } from "hooks/store"; +import { EmptyStateType } from "constants/empty-state"; export const ProjectViewsList = observer(() => { // states const [query, setQuery] = useState(""); - // theme - const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: { toggleCreateViewModal }, } = useApplication(); - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); const { projectViewIds, getViewById, loader } = useProjectView(); if (loader || !projectViewIds) return ; const viewsList = projectViewIds.map((viewId) => getViewById(viewId)); - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "views", isLightMode); - const filteredViewsList = viewsList.filter((v) => v?.name.toLowerCase().includes(query.toLowerCase())); - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - return ( <> {viewsList.length > 0 ? ( @@ -65,21 +52,7 @@ export const ProjectViewsList = observer(() => {
) : ( - toggleCreateViewModal(true), - }} - size="lg" - disabled={!isEditingAllowed} - /> + toggleCreateViewModal(true)} /> )} ); diff --git a/web/constants/empty-state.ts b/web/constants/empty-state.ts index a1b2b06f3..38f334b20 100644 --- a/web/constants/empty-state.ts +++ b/web/constants/empty-state.ts @@ -1,366 +1,516 @@ -// workspace empty state -export const WORKSPACE_EMPTY_STATE_DETAILS = { - dashboard: { +import { EUserProjectRoles } from "./project"; +import { EUserWorkspaceRoles } from "./workspace"; + +export interface EmptyStateDetails { + key: string; + title?: string; + description?: string; + path?: string; + primaryButton?: { + icon?: any; + text: string; + comicBox?: { + title?: string; + description?: string; + }; + }; + secondaryButton?: { + icon?: any; + text: string; + comicBox?: { + title?: string; + description?: string; + }; + }; + accessType?: "workspace" | "project"; + access?: EUserWorkspaceRoles | EUserProjectRoles; +} + +export type EmptyStateKeys = keyof typeof emptyStateDetails; + +export enum EmptyStateType { + WORKSPACE_DASHBOARD = "workspace-dashboard", + WORKSPACE_ANALYTICS = "workspace-analytics", + WORKSPACE_PROJECTS = "workspace-projects", + WORKSPACE_ALL_ISSUES = "workspace-all-issues", + WORKSPACE_ASSIGNED = "workspace-assigned", + WORKSPACE_CREATED = "workspace-created", + WORKSPACE_SUBSCRIBED = "workspace-subscribed", + WORKSPACE_CUSTOM_VIEW = "workspace-custom-view", + WORKSPACE_NO_PROJECTS = "workspace-no-projects", + WORKSPACE_SETTINGS_API_TOKENS = "workspace-settings-api-tokens", + WORKSPACE_SETTINGS_WEBHOOKS = "workspace-settings-webhooks", + WORKSPACE_SETTINGS_EXPORT = "workspace-settings-export", + WORKSPACE_SETTINGS_IMPORT = "workspace-settings-import", + PROFILE_ASSIGNED = "profile-assigned", + PROFILE_CREATED = "profile-created", + PROFILE_SUBSCRIBED = "profile-subscribed", + PROJECT_SETTINGS_LABELS = "project-settings-labels", + PROJECT_SETTINGS_INTEGRATIONS = "project-settings-integrations", + PROJECT_SETTINGS_ESTIMATE = "project-settings-estimate", + PROJECT_CYCLES = "project-cycles", + PROJECT_CYCLE_NO_ISSUES = "project-cycle-no-issues", + PROJECT_CYCLE_ACTIVE = "project-cycle-active", + PROJECT_CYCLE_UPCOMING = "project-cycle-upcoming", + PROJECT_CYCLE_COMPLETED = "project-cycle-completed", + PROJECT_CYCLE_DRAFT = "project-cycle-draft", + PROJECT_EMPTY_FILTER = "project-empty-filter", + PROJECT_ARCHIVED_EMPTY_FILTER = "project-archived-empty-filter", + PROJECT_DRAFT_EMPTY_FILTER = "project-draft-empty-filter", + PROJECT_NO_ISSUES = "project-no-issues", + PROJECT_ARCHIVED_NO_ISSUES = "project-archived-no-issues", + PROJECT_DRAFT_NO_ISSUES = "project-draft-no-issues", + VIEWS_EMPTY_SEARCH = "views-empty-search", + PROJECTS_EMPTY_SEARCH = "projects-empty-search", + COMMANDK_EMPTY_SEARCH = "commandK-empty-search", + MEMBERS_EMPTY_SEARCH = "members-empty-search", + PROJECT_MODULE_ISSUES = "project-module-issues", + PROJECT_MODULE = "project-module", + PROJECT_VIEW = "project-view", + PROJECT_PAGE = "project-page", + PROJECT_PAGE_ALL = "project-page-all", + PROJECT_PAGE_FAVORITE = "project-page-favorite", + PROJECT_PAGE_PRIVATE = "project-page-private", + PROJECT_PAGE_SHARED = "project-page-shared", + PROJECT_PAGE_ARCHIVED = "project-page-archived", + PROJECT_PAGE_RECENT = "project-page-recent", +} + +const emptyStateDetails = { + // workspace + "workspace-dashboard": { + key: "workspace-dashboard", title: "Overview of your projects, activity, and metrics", description: " Welcome to Plane, we are excited to have you here. Create your first project and track your issues, and this page will transform into a space that helps you progress. Admins will also see items which help their team progress.", + path: "/empty-state/onboarding/dashboard", + // path: "/empty-state/onboarding/", primaryButton: { text: "Build your first project", + comicBox: { + title: "Everything starts with a project in Plane", + description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.", + }, }, - comicBox: { - title: "Everything starts with a project in Plane", - description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.", - }, + + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - analytics: { + "workspace-analytics": { + key: "workspace-analytics", title: "Track progress, workloads, and allocations. Spot trends, remove blockers, and move work faster", description: "See scope versus demand, estimates, and scope creep. Get performance by team members and teams, and make sure your project runs on time.", + path: "/empty-state/onboarding/analytics", primaryButton: { text: "Create Cycles and Modules first", + comicBox: { + title: "Analytics works best with Cycles + Modules", + description: + "First, timebox your issues into Cycles and, if you can, group issues that span more than a cycle into Modules. Check out both on the left nav.", + }, }, - comicBox: { - title: "Analytics works best with Cycles + Modules", - description: - "First, timebox your issues into Cycles and, if you can, group issues that span more than a cycle into Modules. Check out both on the left nav.", - }, + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - projects: { + "workspace-projects": { + key: "workspace-projects", title: "Start a Project", description: "Think of each project as the parent for goal-oriented work. Projects are where Jobs, Cycles, and Modules live and, along with your colleagues, help you achieve that goal.", + path: "/empty-state/onboarding/projects", primaryButton: { text: "Start your first project", + comicBox: { + title: "Everything starts with a project in Plane", + description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.", + }, }, - comicBox: { - title: "Everything starts with a project in Plane", - description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.", - }, + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - "assigned-notification": { - key: "assigned-notification", - title: "No issues assigned", - description: "Updates for issues assigned to you can be seen here", - }, - "created-notification": { - key: "created-notification", - title: "No updates to issues", - description: "Updates to issues created by you can be seen here", - }, - "subscribed-notification": { - key: "subscribed-notification", - title: "No updates to issues", - description: "Updates to any issue you are subscribed to can be seen here", - }, -}; - -export const ALL_ISSUES_EMPTY_STATE_DETAILS = { - "all-issues": { - key: "all-issues", + // all-issues + "workspace-all-issues": { + key: "workspace-all-issues", title: "No issues in the project", description: "First project done! Now, slice your work into trackable pieces with issues. Let's go!", + path: "/empty-state/all-issues/all-issues", + primaryButton: { + text: "Create new issue", + }, + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - assigned: { - key: "assigned", + "workspace-assigned": { + key: "workspace-assigned", title: "No issues yet", description: "Issues assigned to you can be tracked from here.", + path: "/empty-state/all-issues/assigned", + primaryButton: { + text: "Create new issue", + }, + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - created: { - key: "created", + "workspace-created": { + key: "workspace-created", title: "No issues yet", description: "All issues created by you come here, track them here directly.", + path: "/empty-state/all-issues/created", + primaryButton: { + text: "Create new issue", + }, + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - subscribed: { - key: "subscribed", + "workspace-subscribed": { + key: "workspace-subscribed", title: "No issues yet", description: "Subscribe to issues you are interested in, track all of them here.", + path: "/empty-state/all-issues/subscribed", }, - "custom-view": { - key: "custom-view", + "workspace-custom-view": { + key: "workspace-custom-view", title: "No issues yet", description: "Issues that applies to the filters, track all of them here.", + path: "/empty-state/all-issues/custom-view", }, -}; - -export const SEARCH_EMPTY_STATE_DETAILS = { - views: { - key: "views", - title: "No matching views", - description: "No views match the search criteria. Create a new view instead.", + "workspace-no-projects": { + key: "workspace-no-projects", + title: "No project", + description: "To create issues or manage your work, you need to create a project or be a part of one.", + path: "/empty-state/onboarding/projects", + primaryButton: { + text: "Start your first project", + comicBox: { + title: "Everything starts with a project in Plane", + description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.", + }, + }, + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - projects: { - key: "projects", - title: "No matching projects", - description: "No projects detected with the matching criteria. Create a new project instead.", - }, - commandK: { - key: "commandK", - title: "No results found. ", - }, - members: { - key: "members", - title: "No matching members", - description: "Add them to the project if they are already a part of the workspace", - }, -}; - -export const WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS = { - "api-tokens": { - key: "api-tokens", + // workspace settings + "workspace-settings-api-tokens": { + key: "workspace-settings-api-tokens", title: "No API tokens created", description: "Plane APIs can be used to integrate your data in Plane with any external system. Create a token to get started.", + path: "/empty-state/workspace-settings/api-tokens", }, - webhooks: { - key: "webhooks", + "workspace-settings-webhooks": { + key: "workspace-settings-webhooks", title: "No webhooks added", description: "Create webhooks to receive real-time updates and automate actions.", + path: "/empty-state/workspace-settings/webhooks", }, - export: { - key: "export", + "workspace-settings-export": { + key: "workspace-settings-export", title: "No previous exports yet", description: "Anytime you export, you will also have a copy here for reference.", + path: "/empty-state/workspace-settings/exports", }, - import: { - key: "export", + "workspace-settings-import": { + key: "workspace-settings-import", title: "No previous imports yet", description: "Find all your previous imports here and download them.", + path: "/empty-state/workspace-settings/imports", }, -}; - -// profile empty state -export const PROFILE_EMPTY_STATE_DETAILS = { - assigned: { - key: "assigned", + // profile + "profile-assigned": { + key: "profile-assigned", title: "No issues are assigned to you", description: "Issues assigned to you can be tracked from here.", + path: "/empty-state/profile/assigned", }, - subscribed: { - key: "created", + "profile-created": { + key: "profile-created", title: "No issues yet", description: "All issues created by you come here, track them here directly.", + path: "/empty-state/profile/created", }, - created: { - key: "subscribed", + "profile-subscribed": { + key: "profile-subscribed", title: "No issues yet", description: "Subscribe to issues you are interested in, track all of them here.", + path: "/empty-state/profile/subscribed", }, -}; - -// project empty state - -export const PROJECT_SETTINGS_EMPTY_STATE_DETAILS = { - labels: { - key: "labels", + // project settings + "project-settings-labels": { + key: "project-settings-labels", title: "No labels yet", description: "Create labels to help organize and filter issues in you project.", + path: "/empty-state/project-settings/labels", }, - integrations: { - key: "integrations", + "project-settings-integrations": { + key: "project-settings-integrations", title: "No integrations configured", description: "Configure GitHub and other integrations to sync your project issues.", + path: "/empty-state/project-settings/integrations", }, - estimate: { - key: "estimate", + "project-settings-estimate": { + key: "project-settings-estimate", title: "No estimates added", description: "Create a set of estimates to communicate the amount of work per issue.", + path: "/empty-state/project-settings/estimates", }, -}; - -export const CYCLE_EMPTY_STATE_DETAILS = { - cycles: { + // project cycles + "project-cycles": { + key: "project-cycles", title: "Group and timebox your work in Cycles.", description: "Break work down by timeboxed chunks, work backwards from your project deadline to set dates, and make tangible progress as a team.", - comicBox: { - title: "Cycles are repetitive time-boxes.", - description: - "A sprint, an iteration, and or any other term you use for weekly or fortnightly tracking of work is a cycle.", - }, + path: "/empty-state/onboarding/cycles", primaryButton: { text: "Set your first cycle", + comicBox: { + title: "Cycles are repetitive time-boxes.", + description: + "A sprint, an iteration, and or any other term you use for weekly or fortnightly tracking of work is a cycle.", + }, }, + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - "no-issues": { - key: "no-issues", + "project-cycle-no-issues": { + key: "project-cycle-no-issues", title: "No issues added to the cycle", description: "Add or create issues you wish to timebox and deliver within this cycle", + path: "/empty-state/cycle-issues/", primaryButton: { text: "Create new issue ", }, secondaryButton: { text: "Add an existing issue", }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, - active: { - key: "active", + "project-cycle-active": { + key: "project-cycle-active", title: "No active cycles", description: "An active cycle includes any period that encompasses today's date within its range. Find the progress and details of the active cycle here.", + path: "/empty-state/cycle/active", }, - upcoming: { - key: "upcoming", + "project-cycle-upcoming": { + key: "project-cycle-upcoming", title: "No upcoming cycles", description: "Upcoming cycles on deck! Just add dates to cycles in draft, and they'll show up right here.", + path: "/empty-state/cycle/upcoming", }, - completed: { - key: "completed", + "project-cycle-completed": { + key: "project-cycle-completed", title: "No completed cycles", description: "Any cycle with a past due date is considered completed. Explore all completed cycles here.", + path: "/empty-state/cycle/completed", }, - draft: { - key: "draft", + "project-cycle-draft": { + key: "project-cycle-draft", title: "No draft cycles", description: "No dates added in cycles? Find them here as drafts.", + path: "/empty-state/cycle/draft", }, -}; - -export const EMPTY_FILTER_STATE_DETAILS = { - archived: { - key: "archived", + // empty filters + "project-empty-filter": { + key: "project-empty-filter", title: "No issues found matching the filters applied", + path: "/empty-state/empty-filters/", secondaryButton: { text: "Clear all filters", }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, - draft: { - key: "draft", + "project-archived-empty-filter": { + key: "project-archived-empty-filter", title: "No issues found matching the filters applied", + path: "/empty-state/empty-filters/", secondaryButton: { text: "Clear all filters", }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, - project: { - key: "project", + "project-draft-empty-filter": { + key: "project-draft-empty-filter", title: "No issues found matching the filters applied", + path: "/empty-state/empty-filters/", secondaryButton: { text: "Clear all filters", }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, -}; - -export const EMPTY_ISSUE_STATE_DETAILS = { - archived: { - key: "archived", - title: "No archived issues yet", - description: - "Archived issues help you remove issues you completed or canceled from focus. You can set automation to auto archive issues and find them here.", - primaryButton: { - text: "Set automation", - }, - }, - draft: { - key: "draft", - title: "No draft issues yet", - description: - "Quickly stepping away but want to keep your place? No worries – save a draft now. Your issues will be right here waiting for you.", - }, - project: { - key: "project", + // project issues + "project-no-issues": { + key: "project-no-issues", title: "Create an issue and assign it to someone, even yourself", description: "Think of issues as jobs, tasks, work, or JTBD. Which we like. An issue and its sub-issues are usually time-based actionables assigned to members of your team. Your team creates, assigns, and completes issues to move your project towards its goal.", - comicBox: { - title: "Issues are building blocks in Plane.", - description: - "Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.", - }, + path: "/empty-state/onboarding/issues", primaryButton: { text: "Create your first issue", + comicBox: { + title: "Issues are building blocks in Plane.", + description: + "Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.", + }, }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, -}; - -export const MODULE_EMPTY_STATE_DETAILS = { - "no-issues": { - key: "no-issues", + "project-archived-no-issues": { + key: "project-archived-no-issues", + title: "No archived issues yet", + description: + "Archived issues help you remove issues you completed or cancelled from focus. You can set automation to auto archive issues and find them here.", + path: "/empty-state/archived/empty-issues", + primaryButton: { + text: "Set automation", + }, + accessType: "project", + access: EUserProjectRoles.MEMBER, + }, + "project-draft-no-issues": { + key: "project-draft-no-issues", + title: "No draft issues yet", + description: + "Quickly stepping away but want to keep your place? No worries – save a draft now. Your issues will be right here waiting for you.", + path: "/empty-state/draft/draft-issues-empty", + }, + "views-empty-search": { + key: "views-empty-search", + title: "No matching views", + description: "No views match the search criteria. Create a new view instead.", + path: "/empty-state/search/search", + }, + "projects-empty-search": { + key: "projects-empty-search", + title: "No matching projects", + description: "No projects detected with the matching criteria. Create a new project instead.", + path: "/empty-state/search/project", + }, + "commandK-empty-search": { + key: "commandK-empty-search", + title: "No results found. ", + path: "/empty-state/search/search", + }, + "members-empty-search": { + key: "members-empty-search", + title: "No matching members", + description: "Add them to the project if they are already a part of the workspace", + path: "/empty-state/search/member", + }, + // project module + "project-module-issues": { + key: "project-modules-issues", title: "No issues in the module", description: "Create or add issues which you want to accomplish as part of this module", + path: "/empty-state/module-issues/", primaryButton: { text: "Create new issue ", }, secondaryButton: { text: "Add an existing issue", }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, - modules: { + "project-module": { + key: "project-module", title: "Map your project milestones to Modules and track aggregated work easily.", description: "A group of issues that belong to a logical, hierarchical parent form a module. Think of them as a way to track work by project milestones. They have their own periods and deadlines as well as analytics to help you see how close or far you are from a milestone.", - - comicBox: { - title: "Modules help group work by hierarchy.", - description: "A cart module, a chassis module, and a warehouse module are all good example of this grouping.", - }, + path: "/empty-state/onboarding/modules", primaryButton: { text: "Build your first module", + comicBox: { + title: "Modules help group work by hierarchy.", + description: "A cart module, a chassis module, and a warehouse module are all good example of this grouping.", + }, }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, -}; - -export const VIEW_EMPTY_STATE_DETAILS = { - "project-views": { + // project views + "project-view": { + key: "project-view", title: "Save filtered views for your project. Create as many as you need", description: "Views are a set of saved filters that you use frequently or want easy access to. All your colleagues in a project can see everyone’s views and choose whichever suits their needs best.", - comicBox: { - title: "Views work atop Issue properties.", - description: "You can create a view from here with as many properties as filters as you see fit.", - }, + path: "/empty-state/onboarding/views", primaryButton: { text: "Create your first view", + comicBox: { + title: "Views work atop Issue properties.", + description: "You can create a view from here with as many properties as filters as you see fit.", + }, }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, -}; - -export const PAGE_EMPTY_STATE_DETAILS = { - pages: { + // project pages + "project-page": { key: "pages", title: "Write a note, a doc, or a full knowledge base. Get Galileo, Plane’s AI assistant, to help you get started", description: "Pages are thoughts potting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your project’s context. To make short work of any doc, invoke Galileo, Plane’s AI, with a shortcut or the click of a button.", + path: "/empty-state/onboarding/pages", primaryButton: { text: "Create your first page", + comicBox: { + title: "A page can be a doc or a doc of docs.", + description: + "We wrote Nikhil and Meera’s love story. You could write your project’s mission, goals, and eventual vision.", + }, }, - comicBox: { - title: "A page can be a doc or a doc of docs.", - description: - "We wrote Nikhil and Meera’s love story. You could write your project’s mission, goals, and eventual vision.", - }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, - All: { - key: "all", + "project-page-all": { + key: "project-page-all", title: "Write a note, a doc, or a full knowledge base", description: "Pages help you organise your thoughts to create wikis, discussions or even document heated takes for your project. Use it wisely!", + path: "/empty-state/pages/all", }, - Favorites: { - key: "favorites", + "project-page-favorite": { + key: "project-page-favorite", title: "No favorite pages yet", description: "Favorites for quick access? mark them and find them right here.", + path: "/empty-state/pages/favorites", }, - Private: { - key: "private", + "project-page-private": { + key: "project-page-private", title: "No private pages yet", description: "Keep your private thoughts here. When you're ready to share, the team's just a click away.", + path: "/empty-state/pages/private", }, - Shared: { - key: "shared", + "project-page-shared": { + key: "project-page-shared", title: "No shared pages yet", description: "See pages shared with everyone in your project right here.", + path: "/empty-state/pages/shared", }, - Archived: { - key: "archived", + "project-page-archived": { + key: "project-page-archived", title: "No archived pages yet", description: "Archive pages not on your radar. Access them here when needed.", + path: "/empty-state/pages/archived", }, - Recent: { - key: "recent", + "project-page-recent": { + key: "project-page-recent", title: "Write a note, a doc, or a full knowledge base", description: "Pages help you organise your thoughts to create wikis, discussions or even document heated takes for your project. Use it wisely! Pages will be sorted and grouped by last updated", + path: "/empty-state/pages/recent", primaryButton: { text: "Create new page", }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, -}; +} as const; + +export const EMPTY_STATE_DETAILS: Record = emptyStateDetails; diff --git a/web/pages/[workspaceSlug]/analytics.tsx b/web/pages/[workspaceSlug]/analytics.tsx index 658f3e34c..c7ee67cab 100644 --- a/web/pages/[workspaceSlug]/analytics.tsx +++ b/web/pages/[workspaceSlug]/analytics.tsx @@ -1,44 +1,33 @@ import React, { Fragment, ReactElement } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import { Tab } from "@headlessui/react"; // hooks +import { useApplication, useEventTracker, useProject, useWorkspace } from "hooks/store"; // layouts +import { AppLayout } from "layouts/app-layout"; // components import { CustomAnalytics, ScopeAndDemand } from "components/analytics"; import { PageHead } from "components/core"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; import { WorkspaceAnalyticsHeader } from "components/headers"; -// constants -import { ANALYTICS_TABS } from "constants/analytics"; -import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { useApplication, useEventTracker, useProject, useUser, useWorkspace } from "hooks/store"; -import { AppLayout } from "layouts/app-layout"; // type import { NextPageWithLayout } from "lib/types"; +// constants +import { ANALYTICS_TABS } from "constants/analytics"; +import { EmptyStateType } from "constants/empty-state"; const AnalyticsPage: NextPageWithLayout = observer(() => { const router = useRouter(); const { analytics_tab } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: { toggleCreateProjectModal }, } = useApplication(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentWorkspaceRole }, - currentUser, - } = useUser(); const { workspaceProjectIds } = useProject(); const { currentWorkspace } = useWorkspace(); // derived values - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "analytics", isLightMode); - const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Analytics` : undefined; return ( @@ -79,22 +68,11 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
) : ( { - setTrackElement("Analytics empty state"); - toggleCreateProjectModal(true); - }, + type={EmptyStateType.WORKSPACE_ANALYTICS} + primaryButtonOnClick={() => { + setTrackElement("Analytics empty state"); + toggleCreateProjectModal(true); }} - comicBox={{ - title: WORKSPACE_EMPTY_STATE_DETAILS["analytics"].comicBox.title, - description: WORKSPACE_EMPTY_STATE_DETAILS["analytics"].comicBox.description, - }} - size="lg" - disabled={!isEditingAllowed} /> )} diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index ac2b760ef..a22e252f2 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -1,39 +1,31 @@ import { Fragment, useCallback, useState, ReactElement } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import { Tab } from "@headlessui/react"; // hooks -import { Tooltip } from "@plane/ui"; -import { PageHead } from "components/core"; -import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { CyclesHeader } from "components/headers"; -import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui"; -import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle"; -import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { useEventTracker, useCycle, useUser, useProject } from "hooks/store"; +import { useEventTracker, useCycle, useProject } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // layouts import { AppLayout } from "layouts/app-layout"; // components +import { PageHead } from "components/core"; +import { CyclesHeader } from "components/headers"; +import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles"; +import { EmptyState } from "components/empty-state"; +import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui"; // ui +import { Tooltip } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; import { TCycleView, TCycleLayout } from "@plane/types"; // constants +import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle"; +import { EmptyStateType } from "constants/empty-state"; const ProjectCyclesPage: NextPageWithLayout = observer(() => { const [createModal, setCreateModal] = useState(false); - // theme - const { resolvedTheme } = useTheme(); // store hooks const { setTrackElement } = useEventTracker(); - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); const { currentProjectCycleIds, loader } = useCycle(); const { getProjectById } = useProject(); // router @@ -43,10 +35,7 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage("cycle_tab", "active"); const { storedValue: cycleLayout, setValue: setCycleLayout } = useLocalStorage("cycle_layout", "list"); // derived values - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "cycles", isLightMode); const totalCycles = currentProjectCycleIds?.length ?? 0; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; const project = projectId ? getProjectById(projectId?.toString()) : undefined; const pageTitle = project?.name ? `${project?.name} - Cycles` : undefined; @@ -89,22 +78,11 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { {totalCycles === 0 ? (
{ + setTrackElement("Cycle empty state"); + setCreateModal(true); }} - primaryButton={{ - text: CYCLE_EMPTY_STATE_DETAILS["cycles"].primaryButton.text, - onClick: () => { - setTrackElement("Cycle empty state"); - setCreateModal(true); - }, - }} - size="lg" - disabled={!isEditingAllowed} />
) : ( diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx index 45204541b..d299c2182 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx @@ -2,18 +2,9 @@ import { useState, Fragment, ReactElement } from "react"; import { observer } from "mobx-react-lite"; import dynamic from "next/dynamic"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import useSWR from "swr"; import { Tab } from "@headlessui/react"; // hooks -import { PageHead } from "components/core"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { PagesHeader } from "components/headers"; -import { RecentPagesList, CreateUpdatePageModal } from "components/pages"; -import { PagesLoader } from "components/ui"; -import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { PAGE_TABS_LIST } from "constants/page"; -import { EUserWorkspaceRoles } from "constants/workspace"; import { useApplication, useEventTracker, useUser, useProject } from "hooks/store"; import { useProjectPages } from "hooks/store/use-project-page"; import useLocalStorage from "hooks/use-local-storage"; @@ -22,9 +13,16 @@ import useSize from "hooks/use-window-size"; // layouts import { AppLayout } from "layouts/app-layout"; // components +import { RecentPagesList, CreateUpdatePageModal } from "components/pages"; +import { EmptyState } from "components/empty-state"; +import { PagesHeader } from "components/headers"; +import { PagesLoader } from "components/ui"; +import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; // constants +import { PAGE_TABS_LIST } from "constants/page"; +import { EmptyStateType } from "constants/empty-state"; const AllPagesList = dynamic(() => import("components/pages").then((a) => a.AllPagesList), { ssr: false, @@ -52,14 +50,8 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => { const { workspaceSlug, projectId } = router.query; // states const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false); - // theme - const { resolvedTheme } = useTheme(); // store hooks - const { - currentUser, - currentUserLoader, - membership: { currentProjectRole }, - } = useUser(); + const { currentUser, currentUserLoader } = useUser(); const { commandPalette: { toggleCreatePageModal }, } = useApplication(); @@ -103,9 +95,6 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => { }; // derived values - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "pages", isLightMode); - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; const project = projectId ? getProjectById(projectId.toString()) : undefined; const pageTitle = project?.name ? `${project?.name} - Pages` : undefined; @@ -216,22 +205,11 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => { ) : ( { - setTrackElement("Pages empty state"); - toggleCreatePageModal(true); - }, + type={EmptyStateType.PROJECT_PAGE} + primaryButtonOnClick={() => { + setTrackElement("Pages empty state"); + toggleCreatePageModal(true); }} - comicBox={{ - title: PAGE_EMPTY_STATE_DETAILS["pages"].comicBox.title, - description: PAGE_EMPTY_STATE_DETAILS["pages"].comicBox.description, - }} - size="lg" - disabled={!isEditingAllowed} /> )} diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx index b227becf9..60e9ca61a 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx @@ -1,17 +1,9 @@ import { ReactElement } from "react"; import { observer } from "mobx-react"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import useSWR from "swr"; // hooks -import { PageHead } from "components/core"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { ProjectSettingHeader } from "components/headers"; -import { IntegrationCard } from "components/project"; import { IntegrationsSettingsLoader } from "components/ui"; -import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys"; -import { useUser } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; @@ -20,10 +12,17 @@ import { NextPageWithLayout } from "lib/types"; import { IntegrationService } from "services/integrations"; import { ProjectService } from "services/project"; // components +import { PageHead } from "components/core"; +import { IntegrationCard } from "components/project"; +import { ProjectSettingHeader } from "components/headers"; +import { EmptyState } from "components/empty-state"; // ui // types import { IProject } from "@plane/types"; // fetch-keys +import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys"; +// constants +import { EmptyStateType } from "constants/empty-state"; // services const integrationService = new IntegrationService(); @@ -32,10 +31,6 @@ const projectService = new ProjectService(); const ProjectIntegrationsPage: NextPageWithLayout = observer(() => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // theme - const { resolvedTheme } = useTheme(); - // store hooks - const { currentUser } = useUser(); // fetch project details const { data: projectDetails } = useSWR( workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, @@ -47,9 +42,6 @@ const ProjectIntegrationsPage: NextPageWithLayout = observer(() => { () => (workspaceSlug ? integrationService.getWorkspaceIntegrationsList(workspaceSlug as string) : null) ); // derived values - const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["integrations"]; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("project-settings", "integrations", isLightMode); const isAdmin = projectDetails?.member_role === 20; const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Integrations` : undefined; @@ -70,15 +62,8 @@ const ProjectIntegrationsPage: NextPageWithLayout = observer(() => { ) : (
router.push(`/${workspaceSlug}/settings/integrations`), - }} - size="lg" - disabled={!isAdmin} + type={EmptyStateType.PROJECT_SETTINGS_INTEGRATIONS} + primaryButtonLink={`/${workspaceSlug}/settings/integrations`} />
) diff --git a/web/pages/[workspaceSlug]/settings/api-tokens.tsx b/web/pages/[workspaceSlug]/settings/api-tokens.tsx index 75d46b63d..59c205968 100644 --- a/web/pages/[workspaceSlug]/settings/api-tokens.tsx +++ b/web/pages/[workspaceSlug]/settings/api-tokens.tsx @@ -1,29 +1,28 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import useSWR from "swr"; // store hooks -import { Button } from "@plane/ui"; -import { ApiTokenListItem, CreateApiTokenModal } from "components/api-token"; -import { PageHead } from "components/core"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { WorkspaceSettingHeader } from "components/headers"; -import { APITokenSettingsLoader } from "components/ui"; -import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { API_TOKENS_LIST } from "constants/fetch-keys"; -import { EUserWorkspaceRoles } from "constants/workspace"; import { useUser, useWorkspace } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // component +import { APITokenSettingsLoader } from "components/ui"; +import { WorkspaceSettingHeader } from "components/headers"; +import { ApiTokenListItem, CreateApiTokenModal } from "components/api-token"; +import { EmptyState } from "components/empty-state"; +import { PageHead } from "components/core"; // ui +import { Button } from "@plane/ui"; // services import { NextPageWithLayout } from "lib/types"; import { APITokenService } from "services/api_token.service"; // types // constants +import { API_TOKENS_LIST } from "constants/fetch-keys"; +import { EUserWorkspaceRoles } from "constants/workspace"; +import { EmptyStateType } from "constants/empty-state"; const apiTokenService = new APITokenService(); @@ -33,12 +32,9 @@ const ApiTokensPage: NextPageWithLayout = observer(() => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { membership: { currentWorkspaceRole }, - currentUser, } = useUser(); const { currentWorkspace } = useWorkspace(); @@ -48,9 +44,6 @@ const ApiTokensPage: NextPageWithLayout = observer(() => { workspaceSlug && isAdmin ? apiTokenService.getApiTokens(workspaceSlug.toString()) : null ); - const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["api-tokens"]; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("workspace-settings", "api-tokens", isLightMode); const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - API Tokens` : undefined; if (!isAdmin) @@ -95,12 +88,7 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
- +
)} diff --git a/web/pages/[workspaceSlug]/settings/webhooks/index.tsx b/web/pages/[workspaceSlug]/settings/webhooks/index.tsx index d5058e29f..24dca325c 100644 --- a/web/pages/[workspaceSlug]/settings/webhooks/index.tsx +++ b/web/pages/[workspaceSlug]/settings/webhooks/index.tsx @@ -1,25 +1,24 @@ import React, { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import useSWR from "swr"; // hooks -import { Button } from "@plane/ui"; -import { PageHead } from "components/core"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { WorkspaceSettingHeader } from "components/headers"; -import { WebhookSettingsLoader } from "components/ui"; -import { WebhooksList, CreateWebhookModal } from "components/web-hooks"; -import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; import { useUser, useWebhook, useWorkspace } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // components +import { PageHead } from "components/core"; +import { WorkspaceSettingHeader } from "components/headers"; +import { WebhookSettingsLoader } from "components/ui"; +import { WebhooksList, CreateWebhookModal } from "components/web-hooks"; +import { EmptyState } from "components/empty-state"; // ui +import { Button } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; // constants +import { EmptyStateType } from "constants/empty-state"; const WebhooksListPage: NextPageWithLayout = observer(() => { // states @@ -27,12 +26,9 @@ const WebhooksListPage: NextPageWithLayout = observer(() => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // theme - const { resolvedTheme } = useTheme(); // mobx store const { membership: { currentWorkspaceRole }, - currentUser, } = useUser(); const { fetchWebhooks, webhooks, clearSecretKey, webhookSecretKey, createWebhook } = useWebhook(); const { currentWorkspace } = useWorkspace(); @@ -44,10 +40,6 @@ const WebhooksListPage: NextPageWithLayout = observer(() => { workspaceSlug && isAdmin ? () => fetchWebhooks(workspaceSlug.toString()) : null ); - const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["webhooks"]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("workspace-settings", "webhooks", isLightMode); const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Webhooks` : undefined; // clear secret key when modal is closed. @@ -99,12 +91,7 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
- +
)} diff --git a/web/public/empty-state/module-issues/gantt-dark-resp.webp b/web/public/empty-state/module-issues/gantt_chart-dark-resp.webp similarity index 100% rename from web/public/empty-state/module-issues/gantt-dark-resp.webp rename to web/public/empty-state/module-issues/gantt_chart-dark-resp.webp diff --git a/web/public/empty-state/module-issues/gantt-dark.webp b/web/public/empty-state/module-issues/gantt_chart-dark.webp similarity index 100% rename from web/public/empty-state/module-issues/gantt-dark.webp rename to web/public/empty-state/module-issues/gantt_chart-dark.webp diff --git a/web/public/empty-state/module-issues/gantt-light-resp.webp b/web/public/empty-state/module-issues/gantt_chart-light-resp.webp similarity index 100% rename from web/public/empty-state/module-issues/gantt-light-resp.webp rename to web/public/empty-state/module-issues/gantt_chart-light-resp.webp diff --git a/web/public/empty-state/module-issues/gantt-light.webp b/web/public/empty-state/module-issues/gantt_chart-light.webp similarity index 100% rename from web/public/empty-state/module-issues/gantt-light.webp rename to web/public/empty-state/module-issues/gantt_chart-light.webp From da735f318a75a058fa1ea0cef8c2261d1fa5e949 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 6 Mar 2024 20:17:32 +0530 Subject: [PATCH 053/308] [WEB-404] chore: calendar layout add existing issue workflow improvement (#3877) * chore: target date none filter * chore: calendar layout add existing issue functionality added for cycle and module * fix: enums export in the types package * chore: remove NestedKeyOf type --------- Co-authored-by: NarayanBavisetti Co-authored-by: Aaryan Khandelwal --- apiserver/plane/app/views/search.py | 4 + packages/types/src/projects.d.ts | 1 + .../calendar/base-calendar-root.tsx | 12 +- .../issue-layouts/calendar/calendar.tsx | 4 + .../issue-layouts/calendar/day-tile.tsx | 6 +- .../calendar/quick-add-issue-form.tsx | 108 +++++++++++++++--- .../calendar/roots/cycle-root.tsx | 18 ++- .../calendar/roots/module-root.tsx | 22 +++- .../issue-layouts/calendar/week-days.tsx | 3 + web/constants/dashboard.ts | 1 - 10 files changed, 147 insertions(+), 32 deletions(-) diff --git a/apiserver/plane/app/views/search.py b/apiserver/plane/app/views/search.py index a2ed1c015..ba8e2e0c3 100644 --- a/apiserver/plane/app/views/search.py +++ b/apiserver/plane/app/views/search.py @@ -235,6 +235,7 @@ class IssueSearchEndpoint(BaseAPIView): cycle = request.query_params.get("cycle", "false") module = request.query_params.get("module", False) sub_issue = request.query_params.get("sub_issue", "false") + target_date = request.query_params.get("target_date", True) issue_id = request.query_params.get("issue_id", False) @@ -273,6 +274,9 @@ class IssueSearchEndpoint(BaseAPIView): if module: issues = issues.exclude(issue_module__module=module) + if target_date == "none": + issues = issues.filter(target_date__isnull=True) + return Response( issues.values( "name", diff --git a/packages/types/src/projects.d.ts b/packages/types/src/projects.d.ts index a93734186..a6da364b9 100644 --- a/packages/types/src/projects.d.ts +++ b/packages/types/src/projects.d.ts @@ -130,6 +130,7 @@ export type TProjectIssuesSearchParams = { sub_issue?: boolean; issue_id?: string; workspace_search: boolean; + target_date?: string; }; export interface ISearchIssueResponse { diff --git a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx index 2a8cbcc26..ab47a7399 100644 --- a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx +++ b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx @@ -29,12 +29,21 @@ interface IBaseCalendarRoot { [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; [EIssueActions.RESTORE]?: (issue: TIssue) => Promise; }; + addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; isCompletedCycle?: boolean; } export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { - const { issueStore, issuesFilterStore, QuickActions, issueActions, viewId, isCompletedCycle = false } = props; + const { + issueStore, + issuesFilterStore, + QuickActions, + issueActions, + addIssuesToView, + viewId, + isCompletedCycle = false, + } = props; // router const router = useRouter(); @@ -128,6 +137,7 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { readOnly={!isEditingAllowed || isCompletedCycle} /> )} + addIssuesToView={addIssuesToView} quickAddCallback={issueStore.quickAddIssue} viewId={viewId} readOnly={!isEditingAllowed || isCompletedCycle} diff --git a/web/components/issues/issue-layouts/calendar/calendar.tsx b/web/components/issues/issue-layouts/calendar/calendar.tsx index 3089a45c4..308393267 100644 --- a/web/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/components/issues/issue-layouts/calendar/calendar.tsx @@ -30,6 +30,7 @@ type Props = { data: TIssue, viewId?: string ) => Promise; + addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; readOnly?: boolean; }; @@ -43,6 +44,7 @@ export const CalendarChart: React.FC = observer((props) => { showWeekends, quickActions, quickAddCallback, + addIssuesToView, viewId, readOnly = false, } = props; @@ -90,6 +92,7 @@ export const CalendarChart: React.FC = observer((props) => { disableIssueCreation={!enableIssueCreation || !isEditingAllowed} quickActions={quickActions} quickAddCallback={quickAddCallback} + addIssuesToView={addIssuesToView} viewId={viewId} readOnly={readOnly} /> @@ -106,6 +109,7 @@ export const CalendarChart: React.FC = observer((props) => { disableIssueCreation={!enableIssueCreation || !isEditingAllowed} quickActions={quickActions} quickAddCallback={quickAddCallback} + addIssuesToView={addIssuesToView} viewId={viewId} readOnly={readOnly} /> diff --git a/web/components/issues/issue-layouts/calendar/day-tile.tsx b/web/components/issues/issue-layouts/calendar/day-tile.tsx index 849b967ce..8ac1e460c 100644 --- a/web/components/issues/issue-layouts/calendar/day-tile.tsx +++ b/web/components/issues/issue-layouts/calendar/day-tile.tsx @@ -4,9 +4,10 @@ import { observer } from "mobx-react-lite"; // components import { CalendarIssueBlocks, ICalendarDate, CalendarQuickAddIssueForm } from "components/issues"; // helpers +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // constants import { MONTHS_LIST } from "constants/calendar"; -import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +// types import { ICycleIssuesFilter } from "store/issue/cycle"; import { IModuleIssuesFilter } from "store/issue/module"; import { IProjectIssuesFilter } from "store/issue/project"; @@ -27,6 +28,7 @@ type Props = { data: TIssue, viewId?: string ) => Promise; + addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; readOnly?: boolean; }; @@ -41,6 +43,7 @@ export const CalendarDayTile: React.FC = observer((props) => { enableQuickIssueCreate, disableIssueCreation, quickAddCallback, + addIssuesToView, viewId, readOnly = false, } = props; @@ -112,6 +115,7 @@ export const CalendarDayTile: React.FC = observer((props) => { target_date: renderFormattedPayloadDate(date.date) ?? undefined, }} quickAddCallback={quickAddCallback} + addIssuesToView={addIssuesToView} viewId={viewId} onOpen={() => setShowAllIssues(true)} /> diff --git a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx index 5738e028e..5f62706dc 100644 --- a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx @@ -2,20 +2,24 @@ import { useEffect, useRef, useState } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { useForm } from "react-hook-form"; +// components +import { ExistingIssuesListModal } from "components/core"; // hooks -import { PlusIcon } from "lucide-react"; -import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; -import { ISSUE_CREATED } from "constants/event-tracker"; -import { createIssuePayload } from "helpers/issue.helper"; -import { useEventTracker, useProject } from "hooks/store"; +import { useEventTracker, useIssueDetail, useProject } from "hooks/store"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // helpers +import { createIssuePayload } from "helpers/issue.helper"; // icons +import { PlusIcon } from "lucide-react"; // ui +import { TOAST_TYPE, setPromiseToast, setToast, CustomMenu } from "@plane/ui"; // types -import { TIssue } from "@plane/types"; +import { ISearchIssueResponse, TIssue } from "@plane/types"; // constants +import { ISSUE_CREATED } from "constants/event-tracker"; +// helper +import { cn } from "helpers/common.helper"; type Props = { formKey: keyof TIssue; @@ -28,6 +32,7 @@ type Props = { data: TIssue, viewId?: string ) => Promise; + addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; onOpen?: () => void; }; @@ -60,21 +65,26 @@ const Inputs = (props: any) => { }; export const CalendarQuickAddIssueForm: React.FC = observer((props) => { - const { formKey, prePopulatedData, quickAddCallback, viewId, onOpen } = props; + const { formKey, prePopulatedData, quickAddCallback, addIssuesToView, viewId, onOpen } = props; // router const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug, projectId, moduleId } = router.query; // store hooks const { getProjectById } = useProject(); const { captureIssueEvent } = useEventTracker(); + const { updateIssue } = useIssueDetail(); // refs const ref = useRef(null); // states const [isOpen, setIsOpen] = useState(false); - + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isExistingIssueModalOpen, setIsExistingIssueModalOpen] = useState(false); // derived values const projectDetail = projectId ? getProjectById(projectId.toString()) : null; + const ExistingIssuesListModalPayload = moduleId + ? { module: moduleId.toString(), target_date: "none" } + : { cycle: true, target_date: "none" }; const { reset, @@ -158,13 +168,50 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { } }; - const handleOpen = () => { + const handleAddIssuesToView = async (data: ISearchIssueResponse[]) => { + if (!workspaceSlug || !projectId) return; + + const issueIds = data.map((i) => i.id); + + try { + // To handle all updates in parallel + await Promise.all( + data.map((issue) => + updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, prePopulatedData ?? {}) + ) + ); + if (addIssuesToView) { + await addIssuesToView(issueIds); + } + } catch (error) { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Something went wrong. Please try again.", + }); + } + }; + + const handleNewIssue = () => { setIsOpen(true); if (onOpen) onOpen(); }; + const handleExistingIssue = () => { + setIsExistingIssueModalOpen(true); + }; return ( <> + {workspaceSlug && projectId && ( + setIsExistingIssueModalOpen(false)} + searchParams={ExistingIssuesListModalPayload} + handleOnSubmit={handleAddIssuesToView} + /> + )} {isOpen && (
= observer((props) => { )} {!isOpen && ( -
- +
+ {addIssuesToView ? ( + setIsMenuOpen(true)} + onMenuClose={() => setIsMenuOpen(false)} + className="w-full" + customButtonClassName="w-full" + customButton={ +
+ + New Issue +
+ } + > + New Issue + Add existing issue +
+ ) : ( + + )}
)} diff --git a/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx b/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx index 7a30d187e..80a21838d 100644 --- a/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx @@ -1,15 +1,16 @@ -import { useMemo } from "react"; +import { useCallback, useMemo } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; //hooks -import { CycleIssueQuickActions } from "components/issues"; -import { EIssuesStoreType } from "constants/issue"; import { useCycle, useIssues } from "hooks/store"; // components +import { CycleIssueQuickActions } from "components/issues"; +import { BaseCalendarRoot } from "../base-calendar-root"; // types import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; -import { BaseCalendarRoot } from "../base-calendar-root"; +// constants +import { EIssuesStoreType } from "constants/issue"; export const CycleCalendarLayout: React.FC = observer(() => { const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); @@ -46,11 +47,20 @@ export const CycleCalendarLayout: React.FC = observer(() => { const isCompletedCycle = cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false; + const addIssuesToView = useCallback( + (issueIds: string[]) => { + if (!workspaceSlug || !projectId || !cycleId) throw new Error(); + return issues.addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds); + }, + [issues?.addIssueToCycle, workspaceSlug, projectId, cycleId] + ); + return ( { const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); const router = useRouter(); - const { workspaceSlug, moduleId } = router.query as { + const { workspaceSlug, projectId, moduleId } = router.query as { workspaceSlug: string; projectId: string; moduleId: string; @@ -42,12 +43,21 @@ export const ModuleCalendarLayout: React.FC = observer(() => { [issues, workspaceSlug, moduleId] ); + const addIssuesToView = useCallback( + (issueIds: string[]) => { + if (!workspaceSlug || !projectId || !moduleId) throw new Error(); + return issues.addIssuesToModule(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), issueIds); + }, + [issues?.addIssuesToModule, workspaceSlug, projectId, moduleId] + ); + return ( ); diff --git a/web/components/issues/issue-layouts/calendar/week-days.tsx b/web/components/issues/issue-layouts/calendar/week-days.tsx index 2ce742fe8..ec1d12e59 100644 --- a/web/components/issues/issue-layouts/calendar/week-days.tsx +++ b/web/components/issues/issue-layouts/calendar/week-days.tsx @@ -25,6 +25,7 @@ type Props = { data: TIssue, viewId?: string ) => Promise; + addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; readOnly?: boolean; }; @@ -39,6 +40,7 @@ export const CalendarWeekDays: React.FC = observer((props) => { enableQuickIssueCreate, disableIssueCreation, quickAddCallback, + addIssuesToView, viewId, readOnly = false, } = props; @@ -68,6 +70,7 @@ export const CalendarWeekDays: React.FC = observer((props) => { enableQuickIssueCreate={enableQuickIssueCreate} disableIssueCreation={disableIssueCreation} quickAddCallback={quickAddCallback} + addIssuesToView={addIssuesToView} viewId={viewId} readOnly={readOnly} /> diff --git a/web/constants/dashboard.ts b/web/constants/dashboard.ts index 3d11b4f12..3d99a4679 100644 --- a/web/constants/dashboard.ts +++ b/web/constants/dashboard.ts @@ -11,7 +11,6 @@ import OverdueIssuesLight from "public/empty-state/dashboard/light/overdue-issue import UpcomingIssuesLight from "public/empty-state/dashboard/light/upcoming-issues.svg"; // types import { TIssuesListTypes, TStateGroups } from "@plane/types"; - // constants import { EUserWorkspaceRoles } from "./workspace"; // icons From 3f1ce9907dc7c16b89b634c62a1db6239437ad8a Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Wed, 6 Mar 2024 20:18:47 +0530 Subject: [PATCH 054/308] fix: auto merge fixes --- .github/workflows/auto-merge.yml | 37 +++++++++++--------------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml index 60ebe5834..ed3814532 100644 --- a/.github/workflows/auto-merge.yml +++ b/.github/workflows/auto-merge.yml @@ -8,13 +8,13 @@ on: env: CURRENT_BRANCH: ${{ github.ref_name }} - SOURCE_BRANCH: ${{ secrets.SYNC_TARGET_BRANCH_NAME }} # The sync branch such as "sync/ce" - TARGET_BRANCH: ${{ secrets.TARGET_BRANCH }} # The target branch that you would like to merge changes like develop + SOURCE_BRANCH: ${{ secrets.SYNC_SOURCE_BRANCH_NAME }} # The sync branch such as "sync/ce" + TARGET_BRANCH: ${{ secrets.SYNC_TARGET_BRANCH_NAME }} # The target branch that you would like to merge changes like develop GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} # Personal access token required to modify contents and workflows - REVIEWER: ${{ secrets.REVIEWER }} + REVIEWER: ${{ secrets.SYNC_PR_REVIEWER }} jobs: - Check_Branch: + Check_Branch: runs-on: ubuntu-latest outputs: BRANCH_MATCH: ${{ steps.check-branch.outputs.MATCH }} @@ -27,7 +27,7 @@ jobs: else echo "MATCH=false" >> $GITHUB_OUTPUT fi - + Auto_Merge: if: ${{ needs.Check_Branch.outputs.BRANCH_MATCH == 'true' }} needs: [Check_Branch] @@ -41,6 +41,11 @@ jobs: with: fetch-depth: 0 # Fetch all history for all branches and tags + - name: Setup Git + run: | + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + - name: Setup GH CLI and Git Config run: | type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y) @@ -50,20 +55,6 @@ jobs: sudo apt update sudo apt install gh -y - - id: git-author - name: Setup Git CLI from Github Token - run: | - VIEWER_JSON=$(gh api graphql -f query='query { viewer { name login databaseId }}' --jq '.data.viewer') - VIEWER_NAME=$(jq --raw-output '.name | values' <<< "${VIEWER_JSON}") - VIEWER_LOGIN=$(jq --raw-output '.login' <<< "${VIEWER_JSON}") - VIEWER_DATABASE_ID=$(jq --raw-output '.databaseId' <<< "${VIEWER_JSON}") - - USER_NAME="${VIEWER_NAME:-${VIEWER_LOGIN}}" - USER_EMAIL="${VIEWER_DATABASE_ID}+${VIEWER_LOGIN}@users.noreply.github.com" - - git config --global user.name ${USER_NAME} - git config --global user.email ${USER_EMAIL} - - name: Check for merge conflicts id: conflicts run: | @@ -88,10 +79,6 @@ jobs: - name: Create PR to Target Branch if: env.HAS_CONFLICTS == 'true' run: | - # Use GitHub CLI to create PR and specify author and committer - PR_URL=$(gh pr create --base $TARGET_BRANCH --head $SOURCE_BRANCH \ - --title "sync: merge conflicts need to be resolved" \ - --body "" \ - --reviewer $REVIEWER ) + # Replace 'username' with the actual GitHub username of the reviewer. + PR_URL=$(gh pr create --base $TARGET_BRANCH --head $SOURCE_BRANCH --title "sync: merge conflicts need to be resolved" --body "" --reviewer $REVIEWER) echo "Pull Request created: $PR_URL" - From cb5198c883e2478252ac2f94f288519945fee691 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 6 Mar 2024 20:38:21 +0530 Subject: [PATCH 055/308] fix: breadcrumb loading state for the issue details page (#3892) Co-authored-by: sriram veeraghanta --- .../project-archived-issue-details.tsx | 10 +++---- .../headers/project-issue-details.tsx | 28 +++++++------------ web/components/project/integration-card.tsx | 1 - 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/web/components/headers/project-archived-issue-details.tsx b/web/components/headers/project-archived-issue-details.tsx index 86dae643d..7cf5c5673 100644 --- a/web/components/headers/project-archived-issue-details.tsx +++ b/web/components/headers/project-archived-issue-details.tsx @@ -13,7 +13,6 @@ import { ProjectLogo } from "components/project"; // ui // types import { IssueArchiveService } from "services/issue"; -import { TIssue } from "@plane/types"; // constants // services // helpers @@ -26,9 +25,9 @@ export const ProjectArchivedIssueDetailsHeader: FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId, archivedIssueId } = router.query; // store hooks - const { currentProjectDetails, getProjectById } = useProject(); + const { currentProjectDetails } = useProject(); - const { data: issueDetails } = useSWR( + const { data: issueDetails } = useSWR( workspaceSlug && projectId && archivedIssueId ? ISSUE_DETAILS(archivedIssueId as string) : null, workspaceSlug && projectId && archivedIssueId ? () => @@ -79,8 +78,9 @@ export const ProjectArchivedIssueDetailsHeader: FC = observer(() => { link={ } diff --git a/web/components/headers/project-issue-details.tsx b/web/components/headers/project-issue-details.tsx index b9343a15c..080a34560 100644 --- a/web/components/headers/project-issue-details.tsx +++ b/web/components/headers/project-issue-details.tsx @@ -1,41 +1,32 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import useSWR from "swr"; // hooks import { PanelRight } from "lucide-react"; import { Breadcrumbs, LayersIcon } from "@plane/ui"; import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { ISSUE_DETAILS } from "constants/fetch-keys"; import { cn } from "helpers/common.helper"; -import { useApplication, useProject } from "hooks/store"; +import { useApplication, useIssueDetail, useProject } from "hooks/store"; // ui // helpers // services -import { IssueService } from "services/issue"; import { ProjectLogo } from "components/project"; // constants // components -// services -const issueService = new IssueService(); - export const ProjectIssueDetailsHeader: FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; // store hooks - const { currentProjectDetails, getProjectById } = useProject(); + const { currentProjectDetails } = useProject(); const { theme: themeStore } = useApplication(); - - const { data: issueDetails } = useSWR( - workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null, - workspaceSlug && projectId && issueId - ? () => issueService.retrieve(workspaceSlug as string, projectId as string, issueId as string) - : null - ); - + const { + issue: { getIssueById }, + } = useIssueDetail(); + // derived values + const issueDetails = issueId ? getIssueById(issueId.toString()) : undefined; const isSidebarCollapsed = themeStore.issueDetailSidebarCollapsed; return ( @@ -77,8 +68,9 @@ export const ProjectIssueDetailsHeader: FC = observer(() => { link={ } diff --git a/web/components/project/integration-card.tsx b/web/components/project/integration-card.tsx index 394238497..17f490bc3 100644 --- a/web/components/project/integration-card.tsx +++ b/web/components/project/integration-card.tsx @@ -13,7 +13,6 @@ import GithubLogo from "public/logos/github-square.png"; import SlackLogo from "public/services/slack.png"; // types import { IWorkspaceIntegration } from "@plane/types"; -// services import { ProjectService } from "services/project"; type Props = { From 549f6d0943333dbc80cce51fa919e772eb19eb11 Mon Sep 17 00:00:00 2001 From: "M. Palanikannan" <73993394+Palanikannan1437@users.noreply.github.com> Date: Wed, 6 Mar 2024 20:38:57 +0530 Subject: [PATCH 056/308] [WEB-438] fix: ai insertion behaviour (#3872) * fixed ai insertion behaviour * replaced all ai popover references to have similar behavior * chore: removed debug statements --- packages/editor/core/package.json | 2 +- .../insert-content-at-cursor-position.ts | 17 +++++++++++++++++ packages/editor/core/src/hooks/use-editor.tsx | 15 +++++++++++++-- .../editor/document-editor/src/ui/index.tsx | 1 + .../editor/rich-text-editor/src/ui/index.tsx | 1 + .../inbox/modals/create-issue-modal.tsx | 4 +--- web/components/issues/issue-modal/form.tsx | 3 +-- .../projects/[projectId]/pages/[pageId].tsx | 9 +++------ web/store/page.store.ts | 6 +++--- yarn.lock | 8 ++++---- 10 files changed, 45 insertions(+), 21 deletions(-) create mode 100644 packages/editor/core/src/helpers/insert-content-at-cursor-position.ts diff --git a/packages/editor/core/package.json b/packages/editor/core/package.json index 198b21b0f..571fb8588 100644 --- a/packages/editor/core/package.json +++ b/packages/editor/core/package.json @@ -53,7 +53,7 @@ "react-moveable": "^0.54.2", "tailwind-merge": "^1.14.0", "tippy.js": "^6.3.7", - "tiptap-markdown": "^0.8.2" + "tiptap-markdown": "^0.8.9" }, "devDependencies": { "@types/node": "18.15.3", diff --git a/packages/editor/core/src/helpers/insert-content-at-cursor-position.ts b/packages/editor/core/src/helpers/insert-content-at-cursor-position.ts new file mode 100644 index 000000000..062acafcb --- /dev/null +++ b/packages/editor/core/src/helpers/insert-content-at-cursor-position.ts @@ -0,0 +1,17 @@ +import { Selection } from "@tiptap/pm/state"; +import { Editor } from "@tiptap/react"; +import { MutableRefObject } from "react"; + +export const insertContentAtSavedSelection = ( + editorRef: MutableRefObject, + content: string, + savedSelection: Selection +) => { + if (editorRef.current && savedSelection) { + editorRef.current + .chain() + .focus() + .insertContentAt(savedSelection?.anchor, content) + .run(); + } +}; diff --git a/packages/editor/core/src/hooks/use-editor.tsx b/packages/editor/core/src/hooks/use-editor.tsx index c2923c1e9..7e6aa5912 100644 --- a/packages/editor/core/src/hooks/use-editor.tsx +++ b/packages/editor/core/src/hooks/use-editor.tsx @@ -1,5 +1,5 @@ import { useEditor as useCustomEditor, Editor } from "@tiptap/react"; -import { useImperativeHandle, useRef, MutableRefObject } from "react"; +import { useImperativeHandle, useRef, MutableRefObject, useState } from "react"; import { CoreEditorProps } from "src/ui/props"; import { CoreEditorExtensions } from "src/ui/extensions"; import { EditorProps } from "@tiptap/pm/view"; @@ -8,6 +8,8 @@ import { DeleteImage } from "src/types/delete-image"; import { IMentionSuggestion } from "src/types/mention-suggestion"; import { RestoreImage } from "src/types/restore-image"; import { UploadImage } from "src/types/upload-image"; +import { Selection } from "@tiptap/pm/state"; +import { insertContentAtSavedSelection } from "src/helpers/insert-content-at-cursor-position"; interface CustomEditorProps { uploadFile: UploadImage; @@ -70,8 +72,10 @@ export const useEditor = ({ onCreate: async ({ editor }) => { onStart?.(editor.getJSON(), getTrimmedHTML(editor.getHTML())); }, + onTransaction: async ({ editor }) => { + setSavedSelection(editor.state.selection); + }, onUpdate: async ({ editor }) => { - // for instant feedback loop setIsSubmitting?.("submitting"); setShouldShowAlert?.(true); onChange?.(editor.getJSON(), getTrimmedHTML(editor.getHTML())); @@ -83,6 +87,8 @@ export const useEditor = ({ const editorRef: MutableRefObject = useRef(null); editorRef.current = editor; + const [savedSelection, setSavedSelection] = useState(null); + useImperativeHandle(forwardedRef, () => ({ clearEditor: () => { editorRef.current?.commands.clearContent(); @@ -90,6 +96,11 @@ export const useEditor = ({ setEditorValue: (content: string) => { editorRef.current?.commands.setContent(content); }, + setEditorValueAtCursorPosition: (content: string) => { + if (savedSelection) { + insertContentAtSavedSelection(editorRef, content, savedSelection); + } + }, })); if (!editor) { diff --git a/packages/editor/document-editor/src/ui/index.tsx b/packages/editor/document-editor/src/ui/index.tsx index 2491e04c7..e9f6d884b 100644 --- a/packages/editor/document-editor/src/ui/index.tsx +++ b/packages/editor/document-editor/src/ui/index.tsx @@ -55,6 +55,7 @@ interface DocumentEditorProps extends IDocumentEditor { interface EditorHandle { clearEditor: () => void; setEditorValue: (content: string) => void; + setEditorValueAtCursorPosition: (content: string) => void; } const DocumentEditor = ({ diff --git a/packages/editor/rich-text-editor/src/ui/index.tsx b/packages/editor/rich-text-editor/src/ui/index.tsx index 4bcb340fd..2aff5d265 100644 --- a/packages/editor/rich-text-editor/src/ui/index.tsx +++ b/packages/editor/rich-text-editor/src/ui/index.tsx @@ -45,6 +45,7 @@ export interface RichTextEditorProps extends IRichTextEditor { interface EditorHandle { clearEditor: () => void; setEditorValue: (content: string) => void; + setEditorValueAtCursorPosition: (content: string) => void; } const RichTextEditor = ({ diff --git a/web/components/inbox/modals/create-issue-modal.tsx b/web/components/inbox/modals/create-issue-modal.tsx index 5a3e614a9..2603b712e 100644 --- a/web/components/inbox/modals/create-issue-modal.tsx +++ b/web/components/inbox/modals/create-issue-modal.tsx @@ -119,9 +119,7 @@ export const CreateInboxIssueModal: React.FC = observer((props) => { const handleAiAssistance = async (response: string) => { if (!workspaceSlug || !projectId) return; - // setValue("description", {}); - setValue("description_html", `${watch("description_html")}

${response}

`); - editorRef.current?.setEditorValue(`${watch("description_html")}`); + editorRef.current?.setEditorValueAtCursorPosition(response); }; const handleAutoGenerateDescription = async () => { diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx index 527ebd0e1..03a9ae5b0 100644 --- a/web/components/issues/issue-modal/form.tsx +++ b/web/components/issues/issue-modal/form.tsx @@ -180,8 +180,7 @@ export const IssueFormRoot: FC = observer((props) => { const handleAiAssistance = async (response: string) => { if (!workspaceSlug || !projectId) return; - setValue("description_html", `${watch("description_html")}

${response}

`); - editorRef.current?.setEditorValue(`${watch("description_html")}`); + editorRef.current?.setEditorValueAtCursorPosition(response); }; const handleAutoGenerateDescription = async () => { diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx index 3a133ee50..16dba79b3 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx @@ -53,7 +53,7 @@ const PageDetailsPage: NextPageWithLayout = observer(() => { membership: { currentProjectRole }, } = useUser(); - const { handleSubmit, setValue, watch, getValues, control, reset } = useForm({ + const { handleSubmit, getValues, control, reset } = useForm({ defaultValues: { name: "", description_html: "" }, }); @@ -124,16 +124,13 @@ const PageDetailsPage: NextPageWithLayout = observer(() => { const updatePage = async (formData: IPage) => { if (!workspaceSlug || !projectId || !pageId) return; - await updateDescriptionAction(formData.description_html); + updateDescriptionAction(formData.description_html); }; const handleAiAssistance = async (response: string) => { if (!workspaceSlug || !projectId || !pageId) return; - const newDescription = `${watch("description_html")}

${response}

`; - setValue("description_html", newDescription); - editorRef.current?.setEditorValue(newDescription); - updateDescriptionAction(newDescription); + editorRef.current?.setEditorValueAtCursorPosition(response); }; const actionCompleteAlert = ({ diff --git a/web/store/page.store.ts b/web/store/page.store.ts index ae416237f..30fc3d157 100644 --- a/web/store/page.store.ts +++ b/web/store/page.store.ts @@ -35,7 +35,7 @@ export interface IPageStore { addToFavorites: () => Promise; removeFromFavorites: () => Promise; updateName: (name: string) => Promise; - updateDescription: (description: string) => Promise; + updateDescription: (description: string) => void; // Reactions disposers: Array<() => void>; @@ -89,7 +89,7 @@ export class PageStore implements IPageStore { addToFavorites: action, removeFromFavorites: action, updateName: action, - updateDescription: action, + updateDescription: action.bound, setIsSubmitting: action, cleanup: action, }); @@ -166,7 +166,7 @@ export class PageStore implements IPageStore { this.name = name; }); - updateDescription = action("updateDescription", async (description_html: string) => { + updateDescription = action("updateDescription", (description_html: string) => { const { projectId, workspaceSlug } = this.rootStore.app.router; if (!projectId || !workspaceSlug) return; diff --git a/yarn.lock b/yarn.lock index c8cfcffd4..66fef83d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8153,10 +8153,10 @@ tippy.js@^6.3.1, tippy.js@^6.3.7: dependencies: "@popperjs/core" "^2.9.0" -tiptap-markdown@^0.8.2: - version "0.8.8" - resolved "https://registry.yarnpkg.com/tiptap-markdown/-/tiptap-markdown-0.8.8.tgz#1e25f40b726239dff84b99a53eb1bdf4af0a02f9" - integrity sha512-I2w/IpvCZ1BoR3nQzG0wRK3uGmDv+Ohyr++G24Ma6RzoDYd0TVGXZp0BOODX5Jj4c6heVY8eksahSeAwJMZBeg== +tiptap-markdown@^0.8.9: + version "0.8.9" + resolved "https://registry.yarnpkg.com/tiptap-markdown/-/tiptap-markdown-0.8.9.tgz#e13f3ae9a1b1649f8c28bb3cae4516a53da7492c" + integrity sha512-TykSDcsb94VFCzPbSSTfB6Kh2HJi7x4B9J3Jm9uSOAMPy8App1YfrLW/rEJLajTxwMVhWBdOo4nidComSlLQsQ== dependencies: "@types/markdown-it" "^12.2.3" markdown-it "^13.0.1" From ed8782757d2ee70779e36d18d2cfecbbd3cd4b69 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Wed, 6 Mar 2024 20:39:50 +0530 Subject: [PATCH 057/308] [WEB - 471] dev: caching users and workspace apis (#3707) * dev: caching users and workspace apis * dev: cache user and config apis * dev: update caching function to use user_id instead of token * dev: update caching layer * dev: update caching logic * dev: format caching file * dev: refactor caching to include name space and user id as key * dev: cache project cover image endpoint --- apiserver/plane/app/views/config.py | 4 +- apiserver/plane/app/views/estimate.py | 5 +- apiserver/plane/app/views/issue.py | 39 +++++---- apiserver/plane/app/views/project.py | 4 +- apiserver/plane/app/views/state.py | 8 +- apiserver/plane/app/views/user.py | 21 ++++- apiserver/plane/app/views/workspace.py | 41 +++++++-- apiserver/plane/license/api/views/instance.py | 20 +++-- apiserver/plane/settings/local.py | 7 +- apiserver/plane/utils/cache.py | 84 +++++++++++++++++++ 10 files changed, 189 insertions(+), 44 deletions(-) create mode 100644 apiserver/plane/utils/cache.py diff --git a/apiserver/plane/app/views/config.py b/apiserver/plane/app/views/config.py index b2a27252c..354f0aebc 100644 --- a/apiserver/plane/app/views/config.py +++ b/apiserver/plane/app/views/config.py @@ -12,13 +12,14 @@ from rest_framework.response import Response # Module imports from .base import BaseAPIView from plane.license.utils.instance_value import get_configuration_value - +from plane.utils.cache import cache_response class ConfigurationEndpoint(BaseAPIView): permission_classes = [ AllowAny, ] + @cache_response(60 * 60 * 2, user=False) def get(self, request): # Get all the configuration ( @@ -136,6 +137,7 @@ class MobileConfigurationEndpoint(BaseAPIView): AllowAny, ] + @cache_response(60 * 60 * 2, user=False) def get(self, request): ( GOOGLE_CLIENT_ID, diff --git a/apiserver/plane/app/views/estimate.py b/apiserver/plane/app/views/estimate.py index 3402bb068..eae2e3351 100644 --- a/apiserver/plane/app/views/estimate.py +++ b/apiserver/plane/app/views/estimate.py @@ -11,7 +11,7 @@ from plane.app.serializers import ( EstimatePointSerializer, EstimateReadSerializer, ) - +from plane.utils.cache import invalidate_cache class ProjectEstimatePointEndpoint(BaseAPIView): permission_classes = [ @@ -49,6 +49,7 @@ class BulkEstimatePointEndpoint(BaseViewSet): serializer = EstimateReadSerializer(estimates, many=True) return Response(serializer.data, status=status.HTTP_200_OK) + @invalidate_cache(path="/api/workspaces/:slug/estimates/", url_params=True, user=False) def create(self, request, slug, project_id): if not request.data.get("estimate", False): return Response( @@ -114,6 +115,7 @@ class BulkEstimatePointEndpoint(BaseViewSet): status=status.HTTP_200_OK, ) + @invalidate_cache(path="/api/workspaces/:slug/estimates/", url_params=True, user=False) def partial_update(self, request, slug, project_id, estimate_id): if not request.data.get("estimate", False): return Response( @@ -182,6 +184,7 @@ class BulkEstimatePointEndpoint(BaseViewSet): status=status.HTTP_200_OK, ) + @invalidate_cache(path="/api/workspaces/:slug/estimates/", url_params=True, user=False) def destroy(self, request, slug, project_id, estimate_id): estimate = Estimate.objects.get( pk=estimate_id, workspace__slug=slug, project_id=project_id diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py index 14e0b6a9a..4355f0ab5 100644 --- a/apiserver/plane/app/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -78,6 +78,7 @@ from plane.bgtasks.issue_activites_task import issue_activity from plane.utils.grouper import group_results from plane.utils.issue_filters import issue_filters from collections import defaultdict +from plane.utils.cache import invalidate_cache class IssueListEndpoint(BaseAPIView): @@ -1001,6 +1002,21 @@ class LabelViewSet(BaseViewSet): ProjectMemberPermission, ] + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(project__project_projectmember__member=self.request.user) + .select_related("project") + .select_related("workspace") + .select_related("parent") + .distinct() + .order_by("sort_order") + ) + + @invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False) def create(self, request, slug, project_id): try: serializer = LabelSerializer(data=request.data) @@ -1020,22 +1036,13 @@ class LabelViewSet(BaseViewSet): status=status.HTTP_400_BAD_REQUEST, ) - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - .select_related("project") - .select_related("workspace") - .select_related("parent") - .distinct() - .order_by("sort_order") - ) + @invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False) + def partial_update(self, request, *args, **kwargs): + return super().partial_update(request, *args, **kwargs) + + @invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False) + def destroy(self, request, *args, **kwargs): + return super().destroy(request, *args, **kwargs) class BulkDeleteIssuesEndpoint(BaseAPIView): diff --git a/apiserver/plane/app/views/project.py b/apiserver/plane/app/views/project.py index 6f9b2618e..42b9c1f37 100644 --- a/apiserver/plane/app/views/project.py +++ b/apiserver/plane/app/views/project.py @@ -65,7 +65,7 @@ from plane.db.models import ( ) from plane.bgtasks.project_invitation_task import project_invitation - +from plane.utils.cache import cache_response class ProjectViewSet(WebhookMixin, BaseViewSet): serializer_class = ProjectListSerializer @@ -1045,6 +1045,8 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView): AllowAny, ] + # Cache the below api for 24 hours + @cache_response(60 * 60 * 24, user=False) def get(self, request): files = [] s3 = boto3.client( diff --git a/apiserver/plane/app/views/state.py b/apiserver/plane/app/views/state.py index 34b3d1dcc..6d4fd7782 100644 --- a/apiserver/plane/app/views/state.py +++ b/apiserver/plane/app/views/state.py @@ -9,14 +9,13 @@ from rest_framework.response import Response from rest_framework import status # Module imports -from . import BaseViewSet, BaseAPIView +from . import BaseViewSet from plane.app.serializers import StateSerializer from plane.app.permissions import ( ProjectEntityPermission, - WorkspaceEntityPermission, ) from plane.db.models import State, Issue - +from plane.utils.cache import invalidate_cache class StateViewSet(BaseViewSet): serializer_class = StateSerializer @@ -41,6 +40,7 @@ class StateViewSet(BaseViewSet): .distinct() ) + @invalidate_cache(path="workspaces/:slug/states/", url_params=True, user=False) def create(self, request, slug, project_id): serializer = StateSerializer(data=request.data) if serializer.is_valid(): @@ -61,6 +61,7 @@ class StateViewSet(BaseViewSet): return Response(state_dict, status=status.HTTP_200_OK) return Response(states, status=status.HTTP_200_OK) + @invalidate_cache(path="workspaces/:slug/states/", url_params=True, user=False) def mark_as_default(self, request, slug, project_id, pk): # Select all the states which are marked as default _ = State.objects.filter( @@ -71,6 +72,7 @@ class StateViewSet(BaseViewSet): ).update(default=True) return Response(status=status.HTTP_204_NO_CONTENT) + @invalidate_cache(path="workspaces/:slug/states/", url_params=True, user=False) def destroy(self, request, slug, project_id, pk): state = State.objects.get( ~Q(name="Triage"), diff --git a/apiserver/plane/app/views/user.py b/apiserver/plane/app/views/user.py index 7764e3b97..07049b8d5 100644 --- a/apiserver/plane/app/views/user.py +++ b/apiserver/plane/app/views/user.py @@ -1,8 +1,10 @@ +# Django imports +from django.db.models import Q, Count, Case, When, IntegerField + # Third party imports from rest_framework.response import Response from rest_framework import status - # Module imports from plane.app.serializers import ( UserSerializer, @@ -15,9 +17,7 @@ from plane.app.views.base import BaseViewSet, BaseAPIView from plane.db.models import User, IssueActivity, WorkspaceMember, ProjectMember from plane.license.models import Instance, InstanceAdmin from plane.utils.paginator import BasePaginator - - -from django.db.models import Q, F, Count, Case, When, IntegerField +from plane.utils.cache import cache_response, invalidate_cache class UserEndpoint(BaseViewSet): @@ -27,6 +27,7 @@ class UserEndpoint(BaseViewSet): def get_object(self): return self.request.user + @cache_response(60 * 60) def retrieve(self, request): serialized_data = UserMeSerializer(request.user).data return Response( @@ -34,10 +35,12 @@ class UserEndpoint(BaseViewSet): status=status.HTTP_200_OK, ) + @cache_response(60 * 60) def retrieve_user_settings(self, request): serialized_data = UserMeSettingsSerializer(request.user).data return Response(serialized_data, status=status.HTTP_200_OK) + @cache_response(60 * 60) def retrieve_instance_admin(self, request): instance = Instance.objects.first() is_admin = InstanceAdmin.objects.filter( @@ -47,6 +50,11 @@ class UserEndpoint(BaseViewSet): {"is_instance_admin": is_admin}, status=status.HTTP_200_OK ) + @invalidate_cache(path="/api/users/me/") + def partial_update(self, request, *args, **kwargs): + return super().partial_update(request, *args, **kwargs) + + @invalidate_cache(path="/api/users/me/") def deactivate(self, request): # Check all workspace user is active user = self.get_object() @@ -145,6 +153,8 @@ class UserEndpoint(BaseViewSet): class UpdateUserOnBoardedEndpoint(BaseAPIView): + + @invalidate_cache(path="/api/users/me/") def patch(self, request): user = User.objects.get(pk=request.user.id, is_active=True) user.is_onboarded = request.data.get("is_onboarded", False) @@ -155,6 +165,8 @@ class UpdateUserOnBoardedEndpoint(BaseAPIView): class UpdateUserTourCompletedEndpoint(BaseAPIView): + + @invalidate_cache(path="/api/users/me/") def patch(self, request): user = User.objects.get(pk=request.user.id, is_active=True) user.is_tour_completed = request.data.get("is_tour_completed", False) @@ -165,6 +177,7 @@ class UpdateUserTourCompletedEndpoint(BaseAPIView): class UserActivityEndpoint(BaseAPIView, BasePaginator): + def get(self, request): queryset = IssueActivity.objects.filter( actor=request.user diff --git a/apiserver/plane/app/views/workspace.py b/apiserver/plane/app/views/workspace.py index 7c4a5db8d..34765c3c7 100644 --- a/apiserver/plane/app/views/workspace.py +++ b/apiserver/plane/app/views/workspace.py @@ -57,6 +57,8 @@ from plane.app.serializers import ( WorkspaceEstimateSerializer, StateSerializer, LabelSerializer, + CycleSerializer, + ModuleSerializer, ) from plane.app.views.base import BaseAPIView from . import BaseViewSet @@ -77,7 +79,6 @@ from plane.db.models import ( Label, WorkspaceMember, CycleIssue, - IssueReaction, WorkspaceUserProperties, Estimate, EstimatePoint, @@ -91,17 +92,11 @@ from plane.app.permissions import ( WorkspaceEntityPermission, WorkspaceViewerPermission, WorkspaceUserPermission, - ProjectLitePermission, ) from plane.bgtasks.workspace_invitation_task import workspace_invitation from plane.utils.issue_filters import issue_filters from plane.bgtasks.event_tracking_task import workspace_invite_event -from plane.app.serializers.module import ( - ModuleSerializer, -) -from plane.app.serializers.cycle import ( - CycleSerializer, -) +from plane.utils.cache import cache_response, invalidate_cache class WorkSpaceViewSet(BaseViewSet): @@ -151,7 +146,8 @@ class WorkSpaceViewSet(BaseViewSet): .annotate(total_issues=issue_count) .select_related("owner") ) - + @invalidate_cache(path="/api/workspaces/", user=False) + @invalidate_cache(path="/api/users/me/workspaces/") def create(self, request): try: serializer = WorkSpaceSerializer(data=request.data) @@ -197,6 +193,20 @@ class WorkSpaceViewSet(BaseViewSet): status=status.HTTP_410_GONE, ) + @cache_response(60 * 60 * 2) + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) + + @invalidate_cache(path="/api/workspaces/", user=False) + @invalidate_cache(path="/api/users/me/workspaces/") + def partial_update(self, request, *args, **kwargs): + return super().partial_update(request, *args, **kwargs) + + @invalidate_cache(path="/api/workspaces/", user=False) + @invalidate_cache(path="/api/users/me/workspaces/") + def destroy(self, request, *args, **kwargs): + return super().destroy(request, *args, **kwargs) + class UserWorkSpacesEndpoint(BaseAPIView): search_fields = [ @@ -206,6 +216,7 @@ class UserWorkSpacesEndpoint(BaseAPIView): "owner", ] + @cache_response(60 * 60 * 2) def get(self, request): fields = [ field @@ -403,6 +414,8 @@ class WorkspaceJoinEndpoint(BaseAPIView): ] """Invitation response endpoint the user can respond to the invitation""" + @invalidate_cache(path="/api/workspaces/", user=False) + @invalidate_cache(path="/api/users/me/workspaces/") def post(self, request, slug, pk): workspace_invite = WorkspaceMemberInvite.objects.get( pk=pk, workspace__slug=slug @@ -499,6 +512,9 @@ class UserWorkspaceInvitationsViewSet(BaseViewSet): .annotate(total_members=Count("workspace__workspace_member")) ) + @invalidate_cache(path="/api/workspaces/", user=False) + @invalidate_cache(path="/api/users/me/workspaces/") + @invalidate_cache(path="/api/workspaces/:slug/members/", url_params=True, user=False) def create(self, request): invitations = request.data.get("invitations", []) workspace_invitations = WorkspaceMemberInvite.objects.filter( @@ -569,6 +585,7 @@ class WorkSpaceMemberViewSet(BaseViewSet): .select_related("member") ) + @cache_response(60 * 60 * 2) def list(self, request, slug): workspace_member = WorkspaceMember.objects.get( member=request.user, @@ -593,6 +610,7 @@ class WorkSpaceMemberViewSet(BaseViewSet): ) return Response(serializer.data, status=status.HTTP_200_OK) + @invalidate_cache(path="/api/workspaces/:slug/members/", url_params=True, user=False) def partial_update(self, request, slug, pk): workspace_member = WorkspaceMember.objects.get( pk=pk, @@ -635,6 +653,7 @@ class WorkSpaceMemberViewSet(BaseViewSet): return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + @invalidate_cache(path="/api/workspaces/:slug/members/", url_params=True, user=False) def destroy(self, request, slug, pk): # Check the user role who is deleting the user workspace_member = WorkspaceMember.objects.get( @@ -699,6 +718,7 @@ class WorkSpaceMemberViewSet(BaseViewSet): workspace_member.save() return Response(status=status.HTTP_204_NO_CONTENT) + @invalidate_cache(path="/api/workspaces/:slug/members/", url_params=True, user=False) def leave(self, request, slug): workspace_member = WorkspaceMember.objects.get( workspace__slug=slug, @@ -1550,6 +1570,7 @@ class WorkspaceLabelsEndpoint(BaseAPIView): WorkspaceViewerPermission, ] + @cache_response(60 * 60 * 2) def get(self, request, slug): labels = Label.objects.filter( workspace__slug=slug, @@ -1565,6 +1586,7 @@ class WorkspaceStatesEndpoint(BaseAPIView): WorkspaceEntityPermission, ] + @cache_response(60 * 60 * 2) def get(self, request, slug): states = State.objects.filter( workspace__slug=slug, @@ -1580,6 +1602,7 @@ class WorkspaceEstimatesEndpoint(BaseAPIView): WorkspaceEntityPermission, ] + @cache_response(60 * 60 * 2) def get(self, request, slug): estimate_ids = Project.objects.filter( workspace__slug=slug, estimate__isnull=False diff --git a/apiserver/plane/license/api/views/instance.py b/apiserver/plane/license/api/views/instance.py index 112c68bc8..c8608cbe5 100644 --- a/apiserver/plane/license/api/views/instance.py +++ b/apiserver/plane/license/api/views/instance.py @@ -1,17 +1,11 @@ # Python imports -import json -import os -import requests import uuid -import random -import string # Django imports from django.utils import timezone from django.contrib.auth.hashers import make_password from django.core.validators import validate_email from django.core.exceptions import ValidationError -from django.conf import settings # Third party imports from rest_framework import status @@ -30,9 +24,9 @@ from plane.license.api.serializers import ( from plane.license.api.permissions import ( InstanceAdminPermission, ) -from plane.db.models import User, WorkspaceMember, ProjectMember +from plane.db.models import User from plane.license.utils.encryption import encrypt_data - +from plane.utils.cache import cache_response, invalidate_cache class InstanceEndpoint(BaseAPIView): def get_permissions(self): @@ -44,6 +38,7 @@ class InstanceEndpoint(BaseAPIView): AllowAny(), ] + @cache_response(60 * 60 * 2, user=False) def get(self, request): instance = Instance.objects.first() # get the instance @@ -58,6 +53,7 @@ class InstanceEndpoint(BaseAPIView): data["is_activated"] = True return Response(data, status=status.HTTP_200_OK) + @invalidate_cache(path="/api/instances/", user=False) def patch(self, request): # Get the instance instance = Instance.objects.first() @@ -75,6 +71,7 @@ class InstanceAdminEndpoint(BaseAPIView): InstanceAdminPermission, ] + @invalidate_cache(path="/api/instances/", user=False) # Create an instance admin def post(self, request): email = request.data.get("email", False) @@ -104,6 +101,7 @@ class InstanceAdminEndpoint(BaseAPIView): serializer = InstanceAdminSerializer(instance_admin) return Response(serializer.data, status=status.HTTP_201_CREATED) + @cache_response(60 * 60 * 2) def get(self, request): instance = Instance.objects.first() if instance is None: @@ -115,6 +113,7 @@ class InstanceAdminEndpoint(BaseAPIView): serializer = InstanceAdminSerializer(instance_admins, many=True) return Response(serializer.data, status=status.HTTP_200_OK) + @invalidate_cache(path="/api/instances/", user=False) def delete(self, request, pk): instance = Instance.objects.first() instance_admin = InstanceAdmin.objects.filter( @@ -128,6 +127,7 @@ class InstanceConfigurationEndpoint(BaseAPIView): InstanceAdminPermission, ] + @cache_response(60 * 60 * 2, user=False) def get(self, request): instance_configurations = InstanceConfiguration.objects.all() serializer = InstanceConfigurationSerializer( @@ -135,6 +135,8 @@ class InstanceConfigurationEndpoint(BaseAPIView): ) return Response(serializer.data, status=status.HTTP_200_OK) + @invalidate_cache(path="/api/configs/", user=False) + @invalidate_cache(path="/api/mobile-configs/", user=False) def patch(self, request): configurations = InstanceConfiguration.objects.filter( key__in=request.data.keys() @@ -170,6 +172,7 @@ class InstanceAdminSignInEndpoint(BaseAPIView): AllowAny, ] + @invalidate_cache(path="/api/instances/", user=False) def post(self, request): # Check instance first instance = Instance.objects.first() @@ -260,6 +263,7 @@ class SignUpScreenVisitedEndpoint(BaseAPIView): AllowAny, ] + @invalidate_cache(path="/api/instances/", user=False) def post(self, request): instance = Instance.objects.first() if instance is None: diff --git a/apiserver/plane/settings/local.py b/apiserver/plane/settings/local.py index 8f27d4234..4dc998e55 100644 --- a/apiserver/plane/settings/local.py +++ b/apiserver/plane/settings/local.py @@ -1,4 +1,5 @@ """Development settings""" + from .common import * # noqa DEBUG = True @@ -14,7 +15,11 @@ EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" CACHES = { "default": { - "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": REDIS_URL, + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + }, } } diff --git a/apiserver/plane/utils/cache.py b/apiserver/plane/utils/cache.py new file mode 100644 index 000000000..dba89c4a6 --- /dev/null +++ b/apiserver/plane/utils/cache.py @@ -0,0 +1,84 @@ +from django.core.cache import cache +# from django.utils.encoding import force_bytes +# import hashlib +from functools import wraps +from rest_framework.response import Response + + +def generate_cache_key(custom_path, auth_header=None): + """Generate a cache key with the given params""" + if auth_header: + key_data = f"{custom_path}:{auth_header}" + else: + key_data = custom_path + return key_data + + +def cache_response(timeout=60 * 60, path=None, user=True): + """decorator to create cache per user""" + + def decorator(view_func): + @wraps(view_func) + def _wrapped_view(instance, request, *args, **kwargs): + # Function to generate cache key + auth_header = ( + None if request.user.is_anonymous else str(request.user.id) if user else None + ) + custom_path = path if path is not None else request.get_full_path() + key = generate_cache_key(custom_path, auth_header) + cached_result = cache.get(key) + if cached_result is not None: + print("Cache Hit") + return Response( + cached_result["data"], status=cached_result["status"] + ) + + print("Cache Miss") + response = view_func(instance, request, *args, **kwargs) + + if response.status_code == 200: + cache.set( + key, + {"data": response.data, "status": response.status_code}, + timeout, + ) + + return response + + return _wrapped_view + + return decorator + + +def invalidate_cache(path=None, url_params=False, user=True): + """invalidate cache per user""" + + def decorator(view_func): + @wraps(view_func) + def _wrapped_view(instance, request, *args, **kwargs): + # Invalidate cache before executing the view function + if url_params: + path_with_values = path + for key, value in kwargs.items(): + path_with_values = path_with_values.replace( + f":{key}", str(value) + ) + + custom_path = path_with_values + else: + custom_path = ( + path if path is not None else request.get_full_path() + ) + + auth_header = ( + None if request.user.is_anonymous else str(request.user.id) if user else None + ) + key = generate_cache_key(custom_path, auth_header) + cache.delete(key) + print("Invalidating cache") + # Execute the view function + return view_func(instance, request, *args, **kwargs) + + return _wrapped_view + + return decorator From a852e3cc523bf368635d4c91cb76ecdeb98defaf Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Wed, 6 Mar 2024 20:41:45 +0530 Subject: [PATCH 058/308] chore: integrations and importers (#3630) * dev: update imports to use jira oauth * dev: remove integration and importer folders and files --- apiserver/plane/app/serializers/__init__.py | 10 - .../app/serializers/integration/__init__.py | 8 - .../plane/app/serializers/integration/base.py | 22 - .../app/serializers/integration/github.py | 45 -- .../app/serializers/integration/slack.py | 14 - apiserver/plane/app/urls/__init__.py | 4 - apiserver/plane/app/urls/external.py | 6 - apiserver/plane/app/urls/importer.py | 37 -- apiserver/plane/app/urls/integration.py | 150 ----- apiserver/plane/app/urls/issue.py | 37 +- apiserver/plane/app/urls/module.py | 6 - apiserver/plane/app/views/__init__.py | 20 - apiserver/plane/app/views/external.py | 7 - apiserver/plane/app/views/importer.py | 558 ------------------ .../plane/app/views/integration/__init__.py | 9 - apiserver/plane/app/views/integration/base.py | 181 ------ .../plane/app/views/integration/github.py | 202 ------- .../plane/app/views/integration/slack.py | 96 --- apiserver/plane/bgtasks/importer_task.py | 201 ------- .../plane/db/models/social_connection.py | 2 +- apiserver/plane/utils/importers/__init__.py | 0 apiserver/plane/utils/importers/jira.py | 117 ---- .../plane/utils/integrations/__init__.py | 0 apiserver/plane/utils/integrations/github.py | 154 ----- apiserver/plane/utils/integrations/slack.py | 21 - 25 files changed, 15 insertions(+), 1892 deletions(-) delete mode 100644 apiserver/plane/app/serializers/integration/__init__.py delete mode 100644 apiserver/plane/app/serializers/integration/base.py delete mode 100644 apiserver/plane/app/serializers/integration/github.py delete mode 100644 apiserver/plane/app/serializers/integration/slack.py delete mode 100644 apiserver/plane/app/urls/importer.py delete mode 100644 apiserver/plane/app/urls/integration.py delete mode 100644 apiserver/plane/app/views/importer.py delete mode 100644 apiserver/plane/app/views/integration/__init__.py delete mode 100644 apiserver/plane/app/views/integration/base.py delete mode 100644 apiserver/plane/app/views/integration/github.py delete mode 100644 apiserver/plane/app/views/integration/slack.py delete mode 100644 apiserver/plane/bgtasks/importer_task.py delete mode 100644 apiserver/plane/utils/importers/__init__.py delete mode 100644 apiserver/plane/utils/importers/jira.py delete mode 100644 apiserver/plane/utils/integrations/__init__.py delete mode 100644 apiserver/plane/utils/integrations/github.py delete mode 100644 apiserver/plane/utils/integrations/slack.py diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index 9bdd4baaf..95651b800 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -86,16 +86,6 @@ from .module import ( from .api import APITokenSerializer, APITokenReadSerializer -from .integration import ( - IntegrationSerializer, - WorkspaceIntegrationSerializer, - GithubIssueSyncSerializer, - GithubRepositorySerializer, - GithubRepositorySyncSerializer, - GithubCommentSyncSerializer, - SlackProjectSyncSerializer, -) - from .importer import ImporterSerializer from .page import ( diff --git a/apiserver/plane/app/serializers/integration/__init__.py b/apiserver/plane/app/serializers/integration/__init__.py deleted file mode 100644 index 112ff02d1..000000000 --- a/apiserver/plane/app/serializers/integration/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .base import IntegrationSerializer, WorkspaceIntegrationSerializer -from .github import ( - GithubRepositorySerializer, - GithubRepositorySyncSerializer, - GithubIssueSyncSerializer, - GithubCommentSyncSerializer, -) -from .slack import SlackProjectSyncSerializer diff --git a/apiserver/plane/app/serializers/integration/base.py b/apiserver/plane/app/serializers/integration/base.py deleted file mode 100644 index 01e484ed0..000000000 --- a/apiserver/plane/app/serializers/integration/base.py +++ /dev/null @@ -1,22 +0,0 @@ -# Module imports -from plane.app.serializers import BaseSerializer -from plane.db.models import Integration, WorkspaceIntegration - - -class IntegrationSerializer(BaseSerializer): - class Meta: - model = Integration - fields = "__all__" - read_only_fields = [ - "verified", - ] - - -class WorkspaceIntegrationSerializer(BaseSerializer): - integration_detail = IntegrationSerializer( - read_only=True, source="integration" - ) - - class Meta: - model = WorkspaceIntegration - fields = "__all__" diff --git a/apiserver/plane/app/serializers/integration/github.py b/apiserver/plane/app/serializers/integration/github.py deleted file mode 100644 index 850bccf1b..000000000 --- a/apiserver/plane/app/serializers/integration/github.py +++ /dev/null @@ -1,45 +0,0 @@ -# Module imports -from plane.app.serializers import BaseSerializer -from plane.db.models import ( - GithubIssueSync, - GithubRepository, - GithubRepositorySync, - GithubCommentSync, -) - - -class GithubRepositorySerializer(BaseSerializer): - class Meta: - model = GithubRepository - fields = "__all__" - - -class GithubRepositorySyncSerializer(BaseSerializer): - repo_detail = GithubRepositorySerializer(source="repository") - - class Meta: - model = GithubRepositorySync - fields = "__all__" - - -class GithubIssueSyncSerializer(BaseSerializer): - class Meta: - model = GithubIssueSync - fields = "__all__" - read_only_fields = [ - "project", - "workspace", - "repository_sync", - ] - - -class GithubCommentSyncSerializer(BaseSerializer): - class Meta: - model = GithubCommentSync - fields = "__all__" - read_only_fields = [ - "project", - "workspace", - "repository_sync", - "issue_sync", - ] diff --git a/apiserver/plane/app/serializers/integration/slack.py b/apiserver/plane/app/serializers/integration/slack.py deleted file mode 100644 index 9c461c5b9..000000000 --- a/apiserver/plane/app/serializers/integration/slack.py +++ /dev/null @@ -1,14 +0,0 @@ -# Module imports -from plane.app.serializers import BaseSerializer -from plane.db.models import SlackProjectSync - - -class SlackProjectSyncSerializer(BaseSerializer): - class Meta: - model = SlackProjectSync - fields = "__all__" - read_only_fields = [ - "project", - "workspace", - "workspace_integration", - ] diff --git a/apiserver/plane/app/urls/__init__.py b/apiserver/plane/app/urls/__init__.py index f2b11f127..40b96687d 100644 --- a/apiserver/plane/app/urls/__init__.py +++ b/apiserver/plane/app/urls/__init__.py @@ -6,9 +6,7 @@ from .cycle import urlpatterns as cycle_urls from .dashboard import urlpatterns as dashboard_urls from .estimate import urlpatterns as estimate_urls from .external import urlpatterns as external_urls -from .importer import urlpatterns as importer_urls from .inbox import urlpatterns as inbox_urls -from .integration import urlpatterns as integration_urls from .issue import urlpatterns as issue_urls from .module import urlpatterns as module_urls from .notification import urlpatterns as notification_urls @@ -32,9 +30,7 @@ urlpatterns = [ *dashboard_urls, *estimate_urls, *external_urls, - *importer_urls, *inbox_urls, - *integration_urls, *issue_urls, *module_urls, *notification_urls, diff --git a/apiserver/plane/app/urls/external.py b/apiserver/plane/app/urls/external.py index 774e6fb7c..8db87a249 100644 --- a/apiserver/plane/app/urls/external.py +++ b/apiserver/plane/app/urls/external.py @@ -2,7 +2,6 @@ from django.urls import path from plane.app.views import UnsplashEndpoint -from plane.app.views import ReleaseNotesEndpoint from plane.app.views import GPTIntegrationEndpoint @@ -12,11 +11,6 @@ urlpatterns = [ UnsplashEndpoint.as_view(), name="unsplash", ), - path( - "release-notes/", - ReleaseNotesEndpoint.as_view(), - name="release-notes", - ), path( "workspaces//projects//ai-assistant/", GPTIntegrationEndpoint.as_view(), diff --git a/apiserver/plane/app/urls/importer.py b/apiserver/plane/app/urls/importer.py deleted file mode 100644 index f3a018d78..000000000 --- a/apiserver/plane/app/urls/importer.py +++ /dev/null @@ -1,37 +0,0 @@ -from django.urls import path - - -from plane.app.views import ( - ServiceIssueImportSummaryEndpoint, - ImportServiceEndpoint, - UpdateServiceImportStatusEndpoint, -) - - -urlpatterns = [ - path( - "workspaces//importers//", - ServiceIssueImportSummaryEndpoint.as_view(), - name="importer-summary", - ), - path( - "workspaces//projects/importers//", - ImportServiceEndpoint.as_view(), - name="importer", - ), - path( - "workspaces//importers/", - ImportServiceEndpoint.as_view(), - name="importer", - ), - path( - "workspaces//importers///", - ImportServiceEndpoint.as_view(), - name="importer", - ), - path( - "workspaces//projects//service//importers//", - UpdateServiceImportStatusEndpoint.as_view(), - name="importer-status", - ), -] diff --git a/apiserver/plane/app/urls/integration.py b/apiserver/plane/app/urls/integration.py deleted file mode 100644 index cf3f82d5a..000000000 --- a/apiserver/plane/app/urls/integration.py +++ /dev/null @@ -1,150 +0,0 @@ -from django.urls import path - - -from plane.app.views import ( - IntegrationViewSet, - WorkspaceIntegrationViewSet, - GithubRepositoriesEndpoint, - GithubRepositorySyncViewSet, - GithubIssueSyncViewSet, - GithubCommentSyncViewSet, - BulkCreateGithubIssueSyncEndpoint, - SlackProjectSyncViewSet, -) - - -urlpatterns = [ - path( - "integrations/", - IntegrationViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="integrations", - ), - path( - "integrations//", - IntegrationViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="integrations", - ), - path( - "workspaces//workspace-integrations/", - WorkspaceIntegrationViewSet.as_view( - { - "get": "list", - } - ), - name="workspace-integrations", - ), - path( - "workspaces//workspace-integrations//", - WorkspaceIntegrationViewSet.as_view( - { - "post": "create", - } - ), - name="workspace-integrations", - ), - path( - "workspaces//workspace-integrations//provider/", - WorkspaceIntegrationViewSet.as_view( - { - "get": "retrieve", - "delete": "destroy", - } - ), - name="workspace-integrations", - ), - # Github Integrations - path( - "workspaces//workspace-integrations//github-repositories/", - GithubRepositoriesEndpoint.as_view(), - ), - path( - "workspaces//projects//workspace-integrations//github-repository-sync/", - GithubRepositorySyncViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - ), - path( - "workspaces//projects//workspace-integrations//github-repository-sync//", - GithubRepositorySyncViewSet.as_view( - { - "get": "retrieve", - "delete": "destroy", - } - ), - ), - path( - "workspaces//projects//github-repository-sync//github-issue-sync/", - GithubIssueSyncViewSet.as_view( - { - "post": "create", - "get": "list", - } - ), - ), - path( - "workspaces//projects//github-repository-sync//bulk-create-github-issue-sync/", - BulkCreateGithubIssueSyncEndpoint.as_view(), - ), - path( - "workspaces//projects//github-repository-sync//github-issue-sync//", - GithubIssueSyncViewSet.as_view( - { - "get": "retrieve", - "delete": "destroy", - } - ), - ), - path( - "workspaces//projects//github-repository-sync//github-issue-sync//github-comment-sync/", - GithubCommentSyncViewSet.as_view( - { - "post": "create", - "get": "list", - } - ), - ), - path( - "workspaces//projects//github-repository-sync//github-issue-sync//github-comment-sync//", - GithubCommentSyncViewSet.as_view( - { - "get": "retrieve", - "delete": "destroy", - } - ), - ), - ## End Github Integrations - # Slack Integration - path( - "workspaces//projects//workspace-integrations//project-slack-sync/", - SlackProjectSyncViewSet.as_view( - { - "post": "create", - "get": "list", - } - ), - ), - path( - "workspaces//projects//workspace-integrations//project-slack-sync//", - SlackProjectSyncViewSet.as_view( - { - "delete": "destroy", - "get": "retrieve", - } - ), - ), - ## End Slack Integration -] diff --git a/apiserver/plane/app/urls/issue.py b/apiserver/plane/app/urls/issue.py index 4ee70450b..6b677287b 100644 --- a/apiserver/plane/app/urls/issue.py +++ b/apiserver/plane/app/urls/issue.py @@ -1,30 +1,27 @@ from django.urls import path - from plane.app.views import ( - IssueListEndpoint, - IssueViewSet, - LabelViewSet, BulkCreateIssueLabelsEndpoint, BulkDeleteIssuesEndpoint, - BulkImportIssuesEndpoint, - UserWorkSpaceIssues, - SubIssuesEndpoint, - IssueLinkViewSet, - IssueAttachmentEndpoint, + CommentReactionViewSet, ExportIssuesEndpoint, IssueActivityEndpoint, - IssueCommentViewSet, - IssueSubscriberViewSet, - IssueReactionViewSet, - CommentReactionViewSet, - IssueUserDisplayPropertyEndpoint, IssueArchiveViewSet, - IssueRelationViewSet, + IssueAttachmentEndpoint, + IssueCommentViewSet, IssueDraftViewSet, + IssueLinkViewSet, + IssueListEndpoint, + IssueReactionViewSet, + IssueRelationViewSet, + IssueSubscriberViewSet, + IssueUserDisplayPropertyEndpoint, + IssueViewSet, + LabelViewSet, + SubIssuesEndpoint, + UserWorkSpaceIssues, ) - urlpatterns = [ path( "workspaces//projects//issues/list/", @@ -85,18 +82,12 @@ urlpatterns = [ BulkDeleteIssuesEndpoint.as_view(), name="project-issues-bulk", ), - path( - "workspaces//projects//bulk-import-issues//", - BulkImportIssuesEndpoint.as_view(), - name="project-issues-bulk", - ), - # deprecated endpoint TODO: remove once confirmed path( "workspaces//my-issues/", UserWorkSpaceIssues.as_view(), name="workspace-issues", ), - ## + ## path( "workspaces//projects//issues//sub-issues/", SubIssuesEndpoint.as_view(), diff --git a/apiserver/plane/app/urls/module.py b/apiserver/plane/app/urls/module.py index 5e9f4f123..981b4d1fb 100644 --- a/apiserver/plane/app/urls/module.py +++ b/apiserver/plane/app/urls/module.py @@ -6,7 +6,6 @@ from plane.app.views import ( ModuleIssueViewSet, ModuleLinkViewSet, ModuleFavoriteViewSet, - BulkImportModulesEndpoint, ModuleUserPropertiesEndpoint, ) @@ -106,11 +105,6 @@ urlpatterns = [ ), name="user-favorite-module", ), - path( - "workspaces//projects//bulk-import-modules//", - BulkImportModulesEndpoint.as_view(), - name="bulk-modules-create", - ), path( "workspaces//projects//modules//user-properties/", ModuleUserPropertiesEndpoint.as_view(), diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 910ea006d..7a311a78d 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -117,25 +117,6 @@ from .module import ( from .api import ApiTokenEndpoint -from .integration import ( - WorkspaceIntegrationViewSet, - IntegrationViewSet, - GithubIssueSyncViewSet, - GithubRepositorySyncViewSet, - GithubCommentSyncViewSet, - GithubRepositoriesEndpoint, - BulkCreateGithubIssueSyncEndpoint, - SlackProjectSyncViewSet, -) - -from .importer import ( - ServiceIssueImportSummaryEndpoint, - ImportServiceEndpoint, - UpdateServiceImportStatusEndpoint, - BulkImportIssuesEndpoint, - BulkImportModulesEndpoint, -) - from .page import ( PageViewSet, PageFavoriteViewSet, @@ -148,7 +129,6 @@ from .search import GlobalSearchEndpoint, IssueSearchEndpoint from .external import ( GPTIntegrationEndpoint, - ReleaseNotesEndpoint, UnsplashEndpoint, ) diff --git a/apiserver/plane/app/views/external.py b/apiserver/plane/app/views/external.py index 618c65e3c..f33e6290f 100644 --- a/apiserver/plane/app/views/external.py +++ b/apiserver/plane/app/views/external.py @@ -18,7 +18,6 @@ from plane.app.serializers import ( ProjectLiteSerializer, WorkspaceLiteSerializer, ) -from plane.utils.integrations.github import get_release_notes from plane.license.utils.instance_value import get_configuration_value @@ -85,12 +84,6 @@ class GPTIntegrationEndpoint(BaseAPIView): ) -class ReleaseNotesEndpoint(BaseAPIView): - def get(self, request): - release_notes = get_release_notes() - return Response(release_notes, status=status.HTTP_200_OK) - - class UnsplashEndpoint(BaseAPIView): def get(self, request): (UNSPLASH_ACCESS_KEY,) = get_configuration_value( diff --git a/apiserver/plane/app/views/importer.py b/apiserver/plane/app/views/importer.py deleted file mode 100644 index a15ed36b7..000000000 --- a/apiserver/plane/app/views/importer.py +++ /dev/null @@ -1,558 +0,0 @@ -# Python imports -import uuid - -# Third party imports -from rest_framework import status -from rest_framework.response import Response - -# Django imports -from django.db.models import Max, Q - -# Module imports -from plane.app.views import BaseAPIView -from plane.db.models import ( - WorkspaceIntegration, - Importer, - APIToken, - Project, - State, - IssueSequence, - Issue, - IssueActivity, - IssueComment, - IssueLink, - IssueLabel, - Workspace, - IssueAssignee, - Module, - ModuleLink, - ModuleIssue, - Label, -) -from plane.app.serializers import ( - ImporterSerializer, - IssueFlatSerializer, - ModuleSerializer, -) -from plane.utils.integrations.github import get_github_repo_details -from plane.utils.importers.jira import ( - jira_project_issue_summary, - is_allowed_hostname, -) -from plane.bgtasks.importer_task import service_importer -from plane.utils.html_processor import strip_tags -from plane.app.permissions import WorkSpaceAdminPermission - - -class ServiceIssueImportSummaryEndpoint(BaseAPIView): - def get(self, request, slug, service): - if service == "github": - owner = request.GET.get("owner", False) - repo = request.GET.get("repo", False) - - if not owner or not repo: - return Response( - {"error": "Owner and repo are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - workspace_integration = WorkspaceIntegration.objects.get( - integration__provider="github", workspace__slug=slug - ) - - access_tokens_url = workspace_integration.metadata.get( - "access_tokens_url", False - ) - - if not access_tokens_url: - return Response( - { - "error": "There was an error during the installation of the GitHub app. To resolve this issue, we recommend reinstalling the GitHub app." - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - issue_count, labels, collaborators = get_github_repo_details( - access_tokens_url, owner, repo - ) - return Response( - { - "issue_count": issue_count, - "labels": labels, - "collaborators": collaborators, - }, - status=status.HTTP_200_OK, - ) - - if service == "jira": - # Check for all the keys - params = { - "project_key": "Project key is required", - "api_token": "API token is required", - "email": "Email is required", - "cloud_hostname": "Cloud hostname is required", - } - - for key, error_message in params.items(): - if not request.GET.get(key, False): - return Response( - {"error": error_message}, - status=status.HTTP_400_BAD_REQUEST, - ) - - project_key = request.GET.get("project_key", "") - api_token = request.GET.get("api_token", "") - email = request.GET.get("email", "") - cloud_hostname = request.GET.get("cloud_hostname", "") - - response = jira_project_issue_summary( - email, api_token, project_key, cloud_hostname - ) - if "error" in response: - return Response(response, status=status.HTTP_400_BAD_REQUEST) - else: - return Response( - response, - status=status.HTTP_200_OK, - ) - return Response( - {"error": "Service not supported yet"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - -class ImportServiceEndpoint(BaseAPIView): - permission_classes = [ - WorkSpaceAdminPermission, - ] - - def post(self, request, slug, service): - project_id = request.data.get("project_id", False) - - if not project_id: - return Response( - {"error": "Project ID is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - workspace = Workspace.objects.get(slug=slug) - - if service == "github": - data = request.data.get("data", False) - metadata = request.data.get("metadata", False) - config = request.data.get("config", False) - if not data or not metadata or not config: - return Response( - {"error": "Data, config and metadata are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - api_token = APIToken.objects.filter( - user=request.user, workspace=workspace - ).first() - if api_token is None: - api_token = APIToken.objects.create( - user=request.user, - label="Importer", - workspace=workspace, - ) - - importer = Importer.objects.create( - service=service, - project_id=project_id, - status="queued", - initiated_by=request.user, - data=data, - metadata=metadata, - token=api_token, - config=config, - created_by=request.user, - updated_by=request.user, - ) - - service_importer.delay(service, importer.id) - serializer = ImporterSerializer(importer) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - if service == "jira": - data = request.data.get("data", False) - metadata = request.data.get("metadata", False) - config = request.data.get("config", False) - - cloud_hostname = metadata.get("cloud_hostname", False) - - if not cloud_hostname: - return Response( - {"error": "Cloud hostname is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - if not is_allowed_hostname(cloud_hostname): - return Response( - {"error": "Hostname is not a valid hostname."}, - status=status.HTTP_400_BAD_REQUEST, - ) - - if not data or not metadata: - return Response( - {"error": "Data, config and metadata are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - api_token = APIToken.objects.filter( - user=request.user, workspace=workspace - ).first() - if api_token is None: - api_token = APIToken.objects.create( - user=request.user, - label="Importer", - workspace=workspace, - ) - - importer = Importer.objects.create( - service=service, - project_id=project_id, - status="queued", - initiated_by=request.user, - data=data, - metadata=metadata, - token=api_token, - config=config, - created_by=request.user, - updated_by=request.user, - ) - - service_importer.delay(service, importer.id) - serializer = ImporterSerializer(importer) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - return Response( - {"error": "Servivce not supported yet"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - def get(self, request, slug): - imports = ( - Importer.objects.filter(workspace__slug=slug) - .order_by("-created_at") - .select_related("initiated_by", "project", "workspace") - ) - serializer = ImporterSerializer(imports, many=True) - return Response(serializer.data) - - def delete(self, request, slug, service, pk): - importer = Importer.objects.get( - pk=pk, service=service, workspace__slug=slug - ) - - if importer.imported_data is not None: - # Delete all imported Issues - imported_issues = importer.imported_data.get("issues", []) - Issue.issue_objects.filter(id__in=imported_issues).delete() - - # Delete all imported Labels - imported_labels = importer.imported_data.get("labels", []) - Label.objects.filter(id__in=imported_labels).delete() - - if importer.service == "jira": - imported_modules = importer.imported_data.get("modules", []) - Module.objects.filter(id__in=imported_modules).delete() - importer.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - def patch(self, request, slug, service, pk): - importer = Importer.objects.get( - pk=pk, service=service, workspace__slug=slug - ) - serializer = ImporterSerializer( - importer, data=request.data, partial=True - ) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -class UpdateServiceImportStatusEndpoint(BaseAPIView): - def post(self, request, slug, project_id, service, importer_id): - importer = Importer.objects.get( - pk=importer_id, - workspace__slug=slug, - project_id=project_id, - service=service, - ) - importer.status = request.data.get("status", "processing") - importer.save() - return Response(status.HTTP_200_OK) - - -class BulkImportIssuesEndpoint(BaseAPIView): - def post(self, request, slug, project_id, service): - # Get the project - project = Project.objects.get(pk=project_id, workspace__slug=slug) - - # Get the default state - default_state = State.objects.filter( - ~Q(name="Triage"), project_id=project_id, default=True - ).first() - # if there is no default state assign any random state - if default_state is None: - default_state = State.objects.filter( - ~Q(name="Triage"), project_id=project_id - ).first() - - # Get the maximum sequence_id - last_id = IssueSequence.objects.filter( - project_id=project_id - ).aggregate(largest=Max("sequence"))["largest"] - - last_id = 1 if last_id is None else last_id + 1 - - # Get the maximum sort order - largest_sort_order = Issue.objects.filter( - project_id=project_id, state=default_state - ).aggregate(largest=Max("sort_order"))["largest"] - - largest_sort_order = ( - 65535 if largest_sort_order is None else largest_sort_order + 10000 - ) - - # Get the issues_data - issues_data = request.data.get("issues_data", []) - - if not len(issues_data): - return Response( - {"error": "Issue data is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Issues - bulk_issues = [] - for issue_data in issues_data: - bulk_issues.append( - Issue( - project_id=project_id, - workspace_id=project.workspace_id, - state_id=issue_data.get("state") - if issue_data.get("state", False) - else default_state.id, - name=issue_data.get("name", "Issue Created through Bulk"), - description_html=issue_data.get( - "description_html", "

" - ), - description_stripped=( - None - if ( - issue_data.get("description_html") == "" - or issue_data.get("description_html") is None - ) - else strip_tags(issue_data.get("description_html")) - ), - sequence_id=last_id, - sort_order=largest_sort_order, - start_date=issue_data.get("start_date", None), - target_date=issue_data.get("target_date", None), - priority=issue_data.get("priority", "none"), - created_by=request.user, - ) - ) - - largest_sort_order = largest_sort_order + 10000 - last_id = last_id + 1 - - issues = Issue.objects.bulk_create( - bulk_issues, - batch_size=100, - ignore_conflicts=True, - ) - - # Sequences - _ = IssueSequence.objects.bulk_create( - [ - IssueSequence( - issue=issue, - sequence=issue.sequence_id, - project_id=project_id, - workspace_id=project.workspace_id, - ) - for issue in issues - ], - batch_size=100, - ) - - # Attach Labels - bulk_issue_labels = [] - for issue, issue_data in zip(issues, issues_data): - labels_list = issue_data.get("labels_list", []) - bulk_issue_labels = bulk_issue_labels + [ - IssueLabel( - issue=issue, - label_id=label_id, - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - ) - for label_id in labels_list - ] - - _ = IssueLabel.objects.bulk_create( - bulk_issue_labels, batch_size=100, ignore_conflicts=True - ) - - # Attach Assignees - bulk_issue_assignees = [] - for issue, issue_data in zip(issues, issues_data): - assignees_list = issue_data.get("assignees_list", []) - bulk_issue_assignees = bulk_issue_assignees + [ - IssueAssignee( - issue=issue, - assignee_id=assignee_id, - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - ) - for assignee_id in assignees_list - ] - - _ = IssueAssignee.objects.bulk_create( - bulk_issue_assignees, batch_size=100, ignore_conflicts=True - ) - - # Track the issue activities - IssueActivity.objects.bulk_create( - [ - IssueActivity( - issue=issue, - actor=request.user, - project_id=project_id, - workspace_id=project.workspace_id, - comment=f"imported the issue from {service}", - verb="created", - created_by=request.user, - ) - for issue in issues - ], - batch_size=100, - ) - - # Create Comments - bulk_issue_comments = [] - for issue, issue_data in zip(issues, issues_data): - comments_list = issue_data.get("comments_list", []) - bulk_issue_comments = bulk_issue_comments + [ - IssueComment( - issue=issue, - comment_html=comment.get("comment_html", "

"), - actor=request.user, - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - ) - for comment in comments_list - ] - - _ = IssueComment.objects.bulk_create( - bulk_issue_comments, batch_size=100 - ) - - # Attach Links - _ = IssueLink.objects.bulk_create( - [ - IssueLink( - issue=issue, - url=issue_data.get("link", {}).get( - "url", "https://github.com" - ), - title=issue_data.get("link", {}).get( - "title", "Original Issue" - ), - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - ) - for issue, issue_data in zip(issues, issues_data) - ] - ) - - return Response( - {"issues": IssueFlatSerializer(issues, many=True).data}, - status=status.HTTP_201_CREATED, - ) - - -class BulkImportModulesEndpoint(BaseAPIView): - def post(self, request, slug, project_id, service): - modules_data = request.data.get("modules_data", []) - project = Project.objects.get(pk=project_id, workspace__slug=slug) - - modules = Module.objects.bulk_create( - [ - Module( - name=module.get("name", uuid.uuid4().hex), - description=module.get("description", ""), - start_date=module.get("start_date", None), - target_date=module.get("target_date", None), - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - ) - for module in modules_data - ], - batch_size=100, - ignore_conflicts=True, - ) - - modules = Module.objects.filter( - id__in=[module.id for module in modules] - ) - - if len(modules) == len(modules_data): - _ = ModuleLink.objects.bulk_create( - [ - ModuleLink( - module=module, - url=module_data.get("link", {}).get( - "url", "https://plane.so" - ), - title=module_data.get("link", {}).get( - "title", "Original Issue" - ), - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - ) - for module, module_data in zip(modules, modules_data) - ], - batch_size=100, - ignore_conflicts=True, - ) - - bulk_module_issues = [] - for module, module_data in zip(modules, modules_data): - module_issues_list = module_data.get("module_issues_list", []) - bulk_module_issues = bulk_module_issues + [ - ModuleIssue( - issue_id=issue, - module=module, - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - ) - for issue in module_issues_list - ] - - _ = ModuleIssue.objects.bulk_create( - bulk_module_issues, batch_size=100, ignore_conflicts=True - ) - - serializer = ModuleSerializer(modules, many=True) - return Response( - {"modules": serializer.data}, status=status.HTTP_201_CREATED - ) - - else: - return Response( - { - "message": "Modules created but issues could not be imported" - }, - status=status.HTTP_200_OK, - ) diff --git a/apiserver/plane/app/views/integration/__init__.py b/apiserver/plane/app/views/integration/__init__.py deleted file mode 100644 index ea20d96ea..000000000 --- a/apiserver/plane/app/views/integration/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from .base import IntegrationViewSet, WorkspaceIntegrationViewSet -from .github import ( - GithubRepositorySyncViewSet, - GithubIssueSyncViewSet, - BulkCreateGithubIssueSyncEndpoint, - GithubCommentSyncViewSet, - GithubRepositoriesEndpoint, -) -from .slack import SlackProjectSyncViewSet diff --git a/apiserver/plane/app/views/integration/base.py b/apiserver/plane/app/views/integration/base.py deleted file mode 100644 index d757fe471..000000000 --- a/apiserver/plane/app/views/integration/base.py +++ /dev/null @@ -1,181 +0,0 @@ -# Python improts -import uuid -import requests - -# Django imports -from django.contrib.auth.hashers import make_password - -# Third party imports -from rest_framework.response import Response -from rest_framework import status -from sentry_sdk import capture_exception - -# Module imports -from plane.app.views import BaseViewSet -from plane.db.models import ( - Integration, - WorkspaceIntegration, - Workspace, - User, - WorkspaceMember, - APIToken, -) -from plane.app.serializers import ( - IntegrationSerializer, - WorkspaceIntegrationSerializer, -) -from plane.utils.integrations.github import ( - get_github_metadata, - delete_github_installation, -) -from plane.app.permissions import WorkSpaceAdminPermission -from plane.utils.integrations.slack import slack_oauth - - -class IntegrationViewSet(BaseViewSet): - serializer_class = IntegrationSerializer - model = Integration - - def create(self, request): - serializer = IntegrationSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def partial_update(self, request, pk): - integration = Integration.objects.get(pk=pk) - if integration.verified: - return Response( - {"error": "Verified integrations cannot be updated"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - serializer = IntegrationSerializer( - integration, data=request.data, partial=True - ) - - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, pk): - integration = Integration.objects.get(pk=pk) - if integration.verified: - return Response( - {"error": "Verified integrations cannot be updated"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - integration.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class WorkspaceIntegrationViewSet(BaseViewSet): - serializer_class = WorkspaceIntegrationSerializer - model = WorkspaceIntegration - - permission_classes = [ - WorkSpaceAdminPermission, - ] - - def get_queryset(self): - return ( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .select_related("integration") - ) - - def create(self, request, slug, provider): - workspace = Workspace.objects.get(slug=slug) - integration = Integration.objects.get(provider=provider) - config = {} - if provider == "github": - installation_id = request.data.get("installation_id", None) - if not installation_id: - return Response( - {"error": "Installation ID is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - metadata = get_github_metadata(installation_id) - config = {"installation_id": installation_id} - - if provider == "slack": - code = request.data.get("code", False) - - if not code: - return Response( - {"error": "Code is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - slack_response = slack_oauth(code=code) - - metadata = slack_response - access_token = metadata.get("access_token", False) - team_id = metadata.get("team", {}).get("id", False) - if not metadata or not access_token or not team_id: - return Response( - { - "error": "Slack could not be installed. Please try again later" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - config = {"team_id": team_id, "access_token": access_token} - - # Create a bot user - bot_user = User.objects.create( - email=f"{uuid.uuid4().hex}@plane.so", - username=uuid.uuid4().hex, - password=make_password(uuid.uuid4().hex), - is_password_autoset=True, - is_bot=True, - first_name=integration.title, - avatar=integration.avatar_url - if integration.avatar_url is not None - else "", - ) - - # Create an API Token for the bot user - api_token = APIToken.objects.create( - user=bot_user, - user_type=1, # bot user - workspace=workspace, - ) - - workspace_integration = WorkspaceIntegration.objects.create( - workspace=workspace, - integration=integration, - actor=bot_user, - api_token=api_token, - metadata=metadata, - config=config, - ) - - # Add bot user as a member of workspace - _ = WorkspaceMember.objects.create( - workspace=workspace_integration.workspace, - member=bot_user, - role=20, - ) - return Response( - WorkspaceIntegrationSerializer(workspace_integration).data, - status=status.HTTP_201_CREATED, - ) - - def destroy(self, request, slug, pk): - workspace_integration = WorkspaceIntegration.objects.get( - pk=pk, workspace__slug=slug - ) - - if workspace_integration.integration.provider == "github": - installation_id = workspace_integration.config.get( - "installation_id", False - ) - if installation_id: - delete_github_installation(installation_id=installation_id) - - workspace_integration.delete() - return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/integration/github.py b/apiserver/plane/app/views/integration/github.py deleted file mode 100644 index 2d37c64b0..000000000 --- a/apiserver/plane/app/views/integration/github.py +++ /dev/null @@ -1,202 +0,0 @@ -# Third party imports -from rest_framework import status -from rest_framework.response import Response -from sentry_sdk import capture_exception - -# Module imports -from plane.app.views import BaseViewSet, BaseAPIView -from plane.db.models import ( - GithubIssueSync, - GithubRepositorySync, - GithubRepository, - WorkspaceIntegration, - ProjectMember, - Label, - GithubCommentSync, - Project, -) -from plane.app.serializers import ( - GithubIssueSyncSerializer, - GithubRepositorySyncSerializer, - GithubCommentSyncSerializer, -) -from plane.utils.integrations.github import get_github_repos -from plane.app.permissions import ( - ProjectBasePermission, - ProjectEntityPermission, -) - - -class GithubRepositoriesEndpoint(BaseAPIView): - permission_classes = [ - ProjectBasePermission, - ] - - def get(self, request, slug, workspace_integration_id): - page = request.GET.get("page", 1) - workspace_integration = WorkspaceIntegration.objects.get( - workspace__slug=slug, pk=workspace_integration_id - ) - - if workspace_integration.integration.provider != "github": - return Response( - {"error": "Not a github integration"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - access_tokens_url = workspace_integration.metadata["access_tokens_url"] - repositories_url = ( - workspace_integration.metadata["repositories_url"] - + f"?per_page=100&page={page}" - ) - repositories = get_github_repos(access_tokens_url, repositories_url) - return Response(repositories, status=status.HTTP_200_OK) - - -class GithubRepositorySyncViewSet(BaseViewSet): - permission_classes = [ - ProjectBasePermission, - ] - - serializer_class = GithubRepositorySyncSerializer - model = GithubRepositorySync - - def perform_create(self, serializer): - serializer.save(project_id=self.kwargs.get("project_id")) - - def get_queryset(self): - return ( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - ) - - def create(self, request, slug, project_id, workspace_integration_id): - name = request.data.get("name", False) - url = request.data.get("url", False) - config = request.data.get("config", {}) - repository_id = request.data.get("repository_id", False) - owner = request.data.get("owner", False) - - if not name or not url or not repository_id or not owner: - return Response( - {"error": "Name, url, repository_id and owner are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Get the workspace integration - workspace_integration = WorkspaceIntegration.objects.get( - pk=workspace_integration_id - ) - - # Delete the old repository object - GithubRepositorySync.objects.filter( - project_id=project_id, workspace__slug=slug - ).delete() - GithubRepository.objects.filter( - project_id=project_id, workspace__slug=slug - ).delete() - - # Create repository - repo = GithubRepository.objects.create( - name=name, - url=url, - config=config, - repository_id=repository_id, - owner=owner, - project_id=project_id, - ) - - # Create a Label for github - label = Label.objects.filter( - name="GitHub", - project_id=project_id, - ).first() - - if label is None: - label = Label.objects.create( - name="GitHub", - project_id=project_id, - description="Label to sync Plane issues with GitHub issues", - color="#003773", - ) - - # Create repo sync - repo_sync = GithubRepositorySync.objects.create( - repository=repo, - workspace_integration=workspace_integration, - actor=workspace_integration.actor, - credentials=request.data.get("credentials", {}), - project_id=project_id, - label=label, - ) - - # Add bot as a member in the project - _ = ProjectMember.objects.get_or_create( - member=workspace_integration.actor, role=20, project_id=project_id - ) - - # Return Response - return Response( - GithubRepositorySyncSerializer(repo_sync).data, - status=status.HTTP_201_CREATED, - ) - - -class GithubIssueSyncViewSet(BaseViewSet): - permission_classes = [ - ProjectEntityPermission, - ] - - serializer_class = GithubIssueSyncSerializer - model = GithubIssueSync - - def perform_create(self, serializer): - serializer.save( - project_id=self.kwargs.get("project_id"), - repository_sync_id=self.kwargs.get("repo_sync_id"), - ) - - -class BulkCreateGithubIssueSyncEndpoint(BaseAPIView): - def post(self, request, slug, project_id, repo_sync_id): - project = Project.objects.get(pk=project_id, workspace__slug=slug) - - github_issue_syncs = request.data.get("github_issue_syncs", []) - github_issue_syncs = GithubIssueSync.objects.bulk_create( - [ - GithubIssueSync( - issue_id=github_issue_sync.get("issue"), - repo_issue_id=github_issue_sync.get("repo_issue_id"), - issue_url=github_issue_sync.get("issue_url"), - github_issue_id=github_issue_sync.get("github_issue_id"), - repository_sync_id=repo_sync_id, - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - updated_by=request.user, - ) - for github_issue_sync in github_issue_syncs - ], - batch_size=100, - ignore_conflicts=True, - ) - - serializer = GithubIssueSyncSerializer(github_issue_syncs, many=True) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - -class GithubCommentSyncViewSet(BaseViewSet): - permission_classes = [ - ProjectEntityPermission, - ] - - serializer_class = GithubCommentSyncSerializer - model = GithubCommentSync - - def perform_create(self, serializer): - serializer.save( - project_id=self.kwargs.get("project_id"), - issue_sync_id=self.kwargs.get("issue_sync_id"), - ) diff --git a/apiserver/plane/app/views/integration/slack.py b/apiserver/plane/app/views/integration/slack.py deleted file mode 100644 index c22ee3e52..000000000 --- a/apiserver/plane/app/views/integration/slack.py +++ /dev/null @@ -1,96 +0,0 @@ -# Django import -from django.db import IntegrityError - -# Third party imports -from rest_framework import status -from rest_framework.response import Response -from sentry_sdk import capture_exception - -# Module imports -from plane.app.views import BaseViewSet, BaseAPIView -from plane.db.models import ( - SlackProjectSync, - WorkspaceIntegration, - ProjectMember, -) -from plane.app.serializers import SlackProjectSyncSerializer -from plane.app.permissions import ( - ProjectBasePermission, - ProjectEntityPermission, -) -from plane.utils.integrations.slack import slack_oauth - - -class SlackProjectSyncViewSet(BaseViewSet): - permission_classes = [ - ProjectBasePermission, - ] - serializer_class = SlackProjectSyncSerializer - model = SlackProjectSync - - def get_queryset(self): - return ( - super() - .get_queryset() - .filter( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), - ) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - ) - - def create(self, request, slug, project_id, workspace_integration_id): - try: - code = request.data.get("code", False) - - if not code: - return Response( - {"error": "Code is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - slack_response = slack_oauth(code=code) - - workspace_integration = WorkspaceIntegration.objects.get( - workspace__slug=slug, pk=workspace_integration_id - ) - - workspace_integration = WorkspaceIntegration.objects.get( - pk=workspace_integration_id, workspace__slug=slug - ) - slack_project_sync = SlackProjectSync.objects.create( - access_token=slack_response.get("access_token"), - scopes=slack_response.get("scope"), - bot_user_id=slack_response.get("bot_user_id"), - webhook_url=slack_response.get("incoming_webhook", {}).get( - "url" - ), - data=slack_response, - team_id=slack_response.get("team", {}).get("id"), - team_name=slack_response.get("team", {}).get("name"), - workspace_integration=workspace_integration, - project_id=project_id, - ) - _ = ProjectMember.objects.get_or_create( - member=workspace_integration.actor, - role=20, - project_id=project_id, - ) - serializer = SlackProjectSyncSerializer(slack_project_sync) - return Response(serializer.data, status=status.HTTP_200_OK) - except IntegrityError as e: - if "already exists" in str(e): - return Response( - {"error": "Slack is already installed for the project"}, - status=status.HTTP_410_GONE, - ) - capture_exception(e) - return Response( - { - "error": "Slack could not be installed. Please try again later" - }, - status=status.HTTP_400_BAD_REQUEST, - ) diff --git a/apiserver/plane/bgtasks/importer_task.py b/apiserver/plane/bgtasks/importer_task.py deleted file mode 100644 index 7a1dc4fc6..000000000 --- a/apiserver/plane/bgtasks/importer_task.py +++ /dev/null @@ -1,201 +0,0 @@ -# Python imports -import json -import requests -import uuid - -# Django imports -from django.conf import settings -from django.core.serializers.json import DjangoJSONEncoder -from django.contrib.auth.hashers import make_password - -# Third Party imports -from celery import shared_task -from sentry_sdk import capture_exception - -# Module imports -from plane.app.serializers import ImporterSerializer -from plane.db.models import ( - Importer, - WorkspaceMember, - GithubRepositorySync, - GithubRepository, - ProjectMember, - WorkspaceIntegration, - Label, - User, - IssueProperty, - UserNotificationPreference, -) - - -@shared_task -def service_importer(service, importer_id): - try: - importer = Importer.objects.get(pk=importer_id) - importer.status = "processing" - importer.save() - - users = importer.data.get("users", []) - - # Check if we need to import users as well - if len(users): - # For all invited users create the users - new_users = User.objects.bulk_create( - [ - User( - email=user.get("email").strip().lower(), - username=uuid.uuid4().hex, - password=make_password(uuid.uuid4().hex), - is_password_autoset=True, - ) - for user in users - if user.get("import", False) == "invite" - ], - batch_size=100, - ignore_conflicts=True, - ) - - _ = UserNotificationPreference.objects.bulk_create( - [UserNotificationPreference(user=user) for user in new_users], - batch_size=100, - ) - - workspace_users = User.objects.filter( - email__in=[ - user.get("email").strip().lower() - for user in users - if user.get("import", False) == "invite" - or user.get("import", False) == "map" - ] - ) - - # Check if any of the users are already member of workspace - _ = WorkspaceMember.objects.filter( - member__in=[user for user in workspace_users], - workspace_id=importer.workspace_id, - ).update(is_active=True) - - # Add new users to Workspace and project automatically - WorkspaceMember.objects.bulk_create( - [ - WorkspaceMember( - member=user, - workspace_id=importer.workspace_id, - created_by=importer.created_by, - ) - for user in workspace_users - ], - batch_size=100, - ignore_conflicts=True, - ) - - ProjectMember.objects.bulk_create( - [ - ProjectMember( - project_id=importer.project_id, - workspace_id=importer.workspace_id, - member=user, - created_by=importer.created_by, - ) - for user in workspace_users - ], - batch_size=100, - ignore_conflicts=True, - ) - - IssueProperty.objects.bulk_create( - [ - IssueProperty( - project_id=importer.project_id, - workspace_id=importer.workspace_id, - user=user, - created_by=importer.created_by, - ) - for user in workspace_users - ], - batch_size=100, - ignore_conflicts=True, - ) - - # Check if sync config is on for github importers - if service == "github" and importer.config.get("sync", False): - name = importer.metadata.get("name", False) - url = importer.metadata.get("url", False) - config = importer.metadata.get("config", {}) - owner = importer.metadata.get("owner", False) - repository_id = importer.metadata.get("repository_id", False) - - workspace_integration = WorkspaceIntegration.objects.get( - workspace_id=importer.workspace_id, - integration__provider="github", - ) - - # Delete the old repository object - GithubRepositorySync.objects.filter( - project_id=importer.project_id - ).delete() - GithubRepository.objects.filter( - project_id=importer.project_id - ).delete() - - # Create a Label for github - label = Label.objects.filter( - name="GitHub", project_id=importer.project_id - ).first() - - if label is None: - label = Label.objects.create( - name="GitHub", - project_id=importer.project_id, - description="Label to sync Plane issues with GitHub issues", - color="#003773", - ) - # Create repository - repo = GithubRepository.objects.create( - name=name, - url=url, - config=config, - repository_id=repository_id, - owner=owner, - project_id=importer.project_id, - ) - - # Create repo sync - _ = GithubRepositorySync.objects.create( - repository=repo, - workspace_integration=workspace_integration, - actor=workspace_integration.actor, - credentials=importer.data.get("credentials", {}), - project_id=importer.project_id, - label=label, - ) - - # Add bot as a member in the project - _ = ProjectMember.objects.get_or_create( - member=workspace_integration.actor, - role=20, - project_id=importer.project_id, - ) - - if settings.PROXY_BASE_URL: - headers = {"Content-Type": "application/json"} - import_data_json = json.dumps( - ImporterSerializer(importer).data, - cls=DjangoJSONEncoder, - ) - _ = requests.post( - f"{settings.PROXY_BASE_URL}/hooks/workspaces/{str(importer.workspace_id)}/projects/{str(importer.project_id)}/importers/{str(service)}/", - json=import_data_json, - headers=headers, - ) - - return - except Exception as e: - importer = Importer.objects.get(pk=importer_id) - importer.status = "failed" - importer.save() - # Print logs if in DEBUG mode - if settings.DEBUG: - print(e) - capture_exception(e) - return diff --git a/apiserver/plane/db/models/social_connection.py b/apiserver/plane/db/models/social_connection.py index 938a73a62..73028e419 100644 --- a/apiserver/plane/db/models/social_connection.py +++ b/apiserver/plane/db/models/social_connection.py @@ -10,7 +10,7 @@ from . import BaseModel class SocialLoginConnection(BaseModel): medium = models.CharField( max_length=20, - choices=(("Google", "google"), ("Github", "github")), + choices=(("Google", "google"), ("Github", "github"), ("Jira", "jira")), default=None, ) last_login_at = models.DateTimeField(default=timezone.now, null=True) diff --git a/apiserver/plane/utils/importers/__init__.py b/apiserver/plane/utils/importers/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/apiserver/plane/utils/importers/jira.py b/apiserver/plane/utils/importers/jira.py deleted file mode 100644 index 6f3a7c217..000000000 --- a/apiserver/plane/utils/importers/jira.py +++ /dev/null @@ -1,117 +0,0 @@ -import requests -import re -from requests.auth import HTTPBasicAuth -from sentry_sdk import capture_exception -from urllib.parse import urlparse, urljoin - - -def is_allowed_hostname(hostname): - allowed_domains = [ - "atl-paas.net", - "atlassian.com", - "atlassian.net", - "jira.com", - ] - parsed_uri = urlparse(f"https://{hostname}") - domain = parsed_uri.netloc.split(":")[0] # Ensures no port is included - base_domain = ".".join(domain.split(".")[-2:]) - return base_domain in allowed_domains - - -def is_valid_project_key(project_key): - if project_key: - project_key = project_key.strip().upper() - # Adjust the regular expression as needed based on your specific requirements. - if len(project_key) > 30: - return False - # Check the validity of the key as well - pattern = re.compile(r"^[A-Z0-9]{1,10}$") - return pattern.match(project_key) is not None - else: - False - - -def generate_valid_project_key(project_key): - return project_key.strip().upper() - - -def generate_url(hostname, path): - if not is_allowed_hostname(hostname): - raise ValueError("Invalid or unauthorized hostname") - return urljoin(f"https://{hostname}", path) - - -def jira_project_issue_summary(email, api_token, project_key, hostname): - try: - if not is_allowed_hostname(hostname): - return {"error": "Invalid or unauthorized hostname"} - - if not is_valid_project_key(project_key): - return {"error": "Invalid project key"} - - auth = HTTPBasicAuth(email, api_token) - headers = {"Accept": "application/json"} - - # make the project key upper case - project_key = generate_valid_project_key(project_key) - - # issues - issue_url = generate_url( - hostname, - f"/rest/api/3/search?jql=project={project_key} AND issuetype!=Epic", - ) - issue_response = requests.request( - "GET", issue_url, headers=headers, auth=auth - ).json()["total"] - - # modules - module_url = generate_url( - hostname, - f"/rest/api/3/search?jql=project={project_key} AND issuetype=Epic", - ) - module_response = requests.request( - "GET", module_url, headers=headers, auth=auth - ).json()["total"] - - # status - status_url = generate_url( - hostname, f"/rest/api/3/project/${project_key}/statuses" - ) - status_response = requests.request( - "GET", status_url, headers=headers, auth=auth - ).json() - - # labels - labels_url = generate_url( - hostname, f"/rest/api/3/label/?jql=project={project_key}" - ) - labels_response = requests.request( - "GET", labels_url, headers=headers, auth=auth - ).json()["total"] - - # users - users_url = generate_url( - hostname, f"/rest/api/3/users/search?jql=project={project_key}" - ) - users_response = requests.request( - "GET", users_url, headers=headers, auth=auth - ).json() - - return { - "issues": issue_response, - "modules": module_response, - "labels": labels_response, - "states": len(status_response), - "users": ( - [ - user - for user in users_response - if user.get("accountType") == "atlassian" - ] - ), - } - except Exception as e: - capture_exception(e) - return { - "error": "Something went wrong could not fetch information from jira" - } diff --git a/apiserver/plane/utils/integrations/__init__.py b/apiserver/plane/utils/integrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/apiserver/plane/utils/integrations/github.py b/apiserver/plane/utils/integrations/github.py deleted file mode 100644 index 5a7ce2aa2..000000000 --- a/apiserver/plane/utils/integrations/github.py +++ /dev/null @@ -1,154 +0,0 @@ -import os -import jwt -import requests -from urllib.parse import urlparse, parse_qs -from datetime import datetime, timedelta -from cryptography.hazmat.primitives.serialization import load_pem_private_key -from cryptography.hazmat.backends import default_backend -from django.conf import settings - - -def get_jwt_token(): - app_id = os.environ.get("GITHUB_APP_ID", "") - secret = bytes( - os.environ.get("GITHUB_APP_PRIVATE_KEY", ""), encoding="utf8" - ) - current_timestamp = int(datetime.now().timestamp()) - due_date = datetime.now() + timedelta(minutes=10) - expiry = int(due_date.timestamp()) - payload = { - "iss": app_id, - "sub": app_id, - "exp": expiry, - "iat": current_timestamp, - "aud": "https://github.com/login/oauth/access_token", - } - - priv_rsakey = load_pem_private_key(secret, None, default_backend()) - token = jwt.encode(payload, priv_rsakey, algorithm="RS256") - return token - - -def get_github_metadata(installation_id): - token = get_jwt_token() - - url = f"https://api.github.com/app/installations/{installation_id}" - headers = { - "Authorization": "Bearer " + str(token), - "Accept": "application/vnd.github+json", - } - response = requests.get(url, headers=headers).json() - return response - - -def get_github_repos(access_tokens_url, repositories_url): - token = get_jwt_token() - - headers = { - "Authorization": "Bearer " + str(token), - "Accept": "application/vnd.github+json", - } - - oauth_response = requests.post( - access_tokens_url, - headers=headers, - ).json() - - oauth_token = oauth_response.get("token", "") - headers = { - "Authorization": "Bearer " + str(oauth_token), - "Accept": "application/vnd.github+json", - } - response = requests.get( - repositories_url, - headers=headers, - ).json() - return response - - -def delete_github_installation(installation_id): - token = get_jwt_token() - - url = f"https://api.github.com/app/installations/{installation_id}" - headers = { - "Authorization": "Bearer " + str(token), - "Accept": "application/vnd.github+json", - } - response = requests.delete(url, headers=headers) - return response - - -def get_github_repo_details(access_tokens_url, owner, repo): - token = get_jwt_token() - - headers = { - "Authorization": "Bearer " + str(token), - "Accept": "application/vnd.github+json", - "X-GitHub-Api-Version": "2022-11-28", - } - - oauth_response = requests.post( - access_tokens_url, - headers=headers, - ).json() - - oauth_token = oauth_response.get("token") - headers = { - "Authorization": "Bearer " + oauth_token, - "Accept": "application/vnd.github+json", - } - open_issues = requests.get( - f"https://api.github.com/repos/{owner}/{repo}", - headers=headers, - ).json()["open_issues_count"] - - total_labels = 0 - - labels_response = requests.get( - f"https://api.github.com/repos/{owner}/{repo}/labels?per_page=100&page=1", - headers=headers, - ) - - # Check if there are more pages - if len(labels_response.links.keys()): - # get the query parameter of last - last_url = labels_response.links.get("last").get("url") - parsed_url = urlparse(last_url) - last_page_value = parse_qs(parsed_url.query)["page"][0] - total_labels = total_labels + 100 * (int(last_page_value) - 1) - - # Get labels in last page - last_page_labels = requests.get(last_url, headers=headers).json() - total_labels = total_labels + len(last_page_labels) - else: - total_labels = len(labels_response.json()) - - # Currently only supporting upto 100 collaborators - # TODO: Update this function to fetch all collaborators - collaborators = requests.get( - f"https://api.github.com/repos/{owner}/{repo}/collaborators?per_page=100&page=1", - headers=headers, - ).json() - - return open_issues, total_labels, collaborators - - -def get_release_notes(): - token = settings.GITHUB_ACCESS_TOKEN - - if token: - headers = { - "Authorization": "Bearer " + str(token), - "Accept": "application/vnd.github.v3+json", - } - else: - headers = { - "Accept": "application/vnd.github.v3+json", - } - url = "https://api.github.com/repos/makeplane/plane/releases?per_page=5&page=1" - response = requests.get(url, headers=headers) - - if response.status_code != 200: - return {"error": "Unable to render information from Github Repository"} - - return response.json() diff --git a/apiserver/plane/utils/integrations/slack.py b/apiserver/plane/utils/integrations/slack.py deleted file mode 100644 index 0cc5b93b2..000000000 --- a/apiserver/plane/utils/integrations/slack.py +++ /dev/null @@ -1,21 +0,0 @@ -import os -import requests - - -def slack_oauth(code): - SLACK_OAUTH_URL = os.environ.get("SLACK_OAUTH_URL", False) - SLACK_CLIENT_ID = os.environ.get("SLACK_CLIENT_ID", False) - SLACK_CLIENT_SECRET = os.environ.get("SLACK_CLIENT_SECRET", False) - - # Oauth Slack - if SLACK_OAUTH_URL and SLACK_CLIENT_ID and SLACK_CLIENT_SECRET: - response = requests.get( - SLACK_OAUTH_URL, - params={ - "code": code, - "client_id": SLACK_CLIENT_ID, - "client_secret": SLACK_CLIENT_SECRET, - }, - ) - return response.json() - return {} From c16a5b9b71071bd6c2ebc2444dc3a417ad3e049a Mon Sep 17 00:00:00 2001 From: rahulramesha <71900764+rahulramesha@users.noreply.github.com> Date: Wed, 6 Mar 2024 20:47:38 +0530 Subject: [PATCH 059/308] [WEB-626] chore: fix sentry issues and refactor issue actions logic for issue layouts (#3650) * restructure the logic to avoid throwing error if any dat is not found * updated files for previous commit * fix build errors * remove throwing error if userId is undefined * optionally chain display_name property to fix sentry issues * add ooptional check * change issue action logic to increase code maintainability and make sure to send only the updated date while updating the issue * fix issue updation bugs * fix module issues build error * fix runtime errors --- .../issues/peek-overview/layout.tsx | 1 - .../scope-and-demand/leaderboard.tsx | 8 +- .../core/sidebar/sidebar-progress-stats.tsx | 4 +- web/components/cycles/active-cycle-stats.tsx | 2 +- web/components/cycles/cycle-mobile-header.tsx | 46 +- .../dashboard/widgets/recent-activity.tsx | 2 +- .../collaborators-list.tsx | 6 +- web/components/headers/module-issues.tsx | 52 +- .../headers/project-view-issues.tsx | 46 +- .../integration/github/single-user-select.tsx | 31 +- .../integration/jira/import-users.tsx | 31 +- web/components/integration/single-import.tsx | 2 +- web/components/issues/issue-detail/root.tsx | 4 +- .../calendar/base-calendar-root.tsx | 85 +-- .../issue-layouts/calendar/calendar.tsx | 14 +- .../calendar/dropdowns/options-dropdown.tsx | 48 +- .../issues/issue-layouts/calendar/header.tsx | 17 +- .../calendar/roots/cycle-root.tsx | 33 +- .../calendar/roots/module-root.tsx | 43 +- .../calendar/roots/project-root.tsx | 44 +- .../calendar/roots/project-view-root.tsx | 21 +- .../issues/issue-layouts/calendar/utils.ts | 16 +- .../applied-filters/roots/cycle-root.tsx | 34 +- .../applied-filters/roots/module-root.tsx | 34 +- .../roots/project-view-root.tsx | 30 +- .../issue-layouts/gantt/base-gantt-root.tsx | 39 +- .../issues/issue-layouts/gantt/cycle-root.tsx | 38 +- .../issue-layouts/gantt/module-root.tsx | 38 +- .../issue-layouts/gantt/project-root.tsx | 27 +- .../issue-layouts/gantt/project-view-root.tsx | 25 +- .../issue-layouts/kanban/base-kanban-root.tsx | 91 +-- .../issues/issue-layouts/kanban/block.tsx | 17 +- .../issue-layouts/kanban/blocks-list.tsx | 7 +- .../issues/issue-layouts/kanban/default.tsx | 19 +- .../kanban/headers/group-by-card.tsx | 4 +- .../issue-layouts/kanban/kanban-group.tsx | 7 +- .../issue-layouts/kanban/roots/cycle-root.tsx | 35 +- .../kanban/roots/draft-issue-root.tsx | 46 +- .../kanban/roots/module-root.tsx | 35 +- .../kanban/roots/profile-issues-root.tsx | 39 +- .../kanban/roots/project-root.tsx | 48 +- .../kanban/roots/project-view-root.tsx | 20 +- .../issues/issue-layouts/kanban/swimlanes.tsx | 27 +- .../issues/issue-layouts/kanban/utils.ts | 54 +- .../issue-layouts/list/base-list-root.tsx | 87 +-- .../issues/issue-layouts/list/block.tsx | 11 +- .../issues/issue-layouts/list/blocks-list.tsx | 7 +- .../issues/issue-layouts/list/default.tsx | 19 +- .../list/headers/group-by-card.tsx | 4 +- .../list/roots/archived-issue-root.tsx | 31 +- .../issue-layouts/list/roots/cycle-root.tsx | 34 +- .../list/roots/draft-issue-root.tsx | 36 +- .../issue-layouts/list/roots/module-root.tsx | 35 +- .../list/roots/profile-issues-root.tsx | 37 +- .../issue-layouts/list/roots/project-root.tsx | 42 +- .../list/roots/project-view-root.tsx | 21 +- .../properties/all-properties.tsx | 159 ++--- .../quick-action-dropdowns/all-issue.tsx | 2 +- .../quick-action-dropdowns/cycle-issue.tsx | 2 +- .../quick-action-dropdowns/module-issue.tsx | 2 +- .../quick-action-dropdowns/project-issue.tsx | 2 +- .../roots/all-issue-layout-root.tsx | 52 +- .../roots/project-view-layout-root.tsx | 30 +- .../spreadsheet/base-spreadsheet-root.tsx | 98 +-- .../spreadsheet/issue-column.tsx | 8 +- .../issue-layouts/spreadsheet/issue-row.tsx | 15 +- .../spreadsheet/roots/cycle-root.tsx | 41 +- .../spreadsheet/roots/module-root.tsx | 38 +- .../spreadsheet/roots/project-root.tsx | 46 +- .../spreadsheet/roots/project-view-root.tsx | 21 +- .../spreadsheet/spreadsheet-table.tsx | 7 +- .../spreadsheet/spreadsheet-view.tsx | 7 +- web/components/issues/issue-modal/form.tsx | 14 +- web/components/issues/issue-modal/modal.tsx | 52 +- web/components/issues/peek-overview/root.tsx | 4 +- web/components/profile/overview/activity.tsx | 10 +- web/components/profile/sidebar.tsx | 17 +- web/components/project/member-list-item.tsx | 34 +- web/components/project/member-select.tsx | 4 +- web/components/project/project-logo.tsx | 4 +- .../project/send-project-invitation-modal.tsx | 49 +- .../confirm-workspace-member-remove.tsx | 4 +- web/helpers/issue.helper.ts | 17 + web/hooks/use-issues-actions.tsx | 576 ++++++++++++++++++ web/pages/profile/index.tsx | 10 +- web/store/issue/archived/issue.store.ts | 2 +- web/store/issue/cycle/filter.store.ts | 5 +- web/store/issue/cycle/issue.store.ts | 63 +- web/store/issue/issue-details/issue.store.ts | 2 +- web/store/issue/module/filter.store.ts | 5 +- web/store/issue/module/issue.store.ts | 55 +- web/store/issue/profile/filter.store.ts | 6 +- web/store/issue/profile/issue.store.ts | 50 +- web/store/issue/project-views/filter.store.ts | 6 +- web/store/issue/project-views/issue.store.ts | 70 +-- web/store/issue/workspace/filter.store.ts | 5 +- web/store/issue/workspace/issue.store.ts | 36 +- web/store/member/workspace-member.store.ts | 2 +- 98 files changed, 1402 insertions(+), 1864 deletions(-) create mode 100644 web/hooks/use-issues-actions.tsx diff --git a/space/components/issues/peek-overview/layout.tsx b/space/components/issues/peek-overview/layout.tsx index 7345b4b28..602277f3e 100644 --- a/space/components/issues/peek-overview/layout.tsx +++ b/space/components/issues/peek-overview/layout.tsx @@ -11,7 +11,6 @@ import { FullScreenPeekView, SidePeekView } from "components/issues/peek-overvie // lib import { useMobxStore } from "lib/mobx/store-provider"; - export const IssuePeekOverview: React.FC = observer(() => { // states const [isSidePeekOpen, setIsSidePeekOpen] = useState(false); diff --git a/web/components/analytics/scope-and-demand/leaderboard.tsx b/web/components/analytics/scope-and-demand/leaderboard.tsx index 9cd38dde4..ae7447b0f 100644 --- a/web/components/analytics/scope-and-demand/leaderboard.tsx +++ b/web/components/analytics/scope-and-demand/leaderboard.tsx @@ -24,7 +24,7 @@ export const AnalyticsLeaderBoard: React.FC = ({ users, title, emptyState
{users.map((user) => ( = ({ users, title, emptyState {user.display_name
) : (
- {user.display_name !== "" ? user?.display_name?.[0] : "?"} + {user?.display_name !== "" ? user?.display_name?.[0] : "?"}
)} - {user.display_name !== "" ? `${user.display_name}` : "No assignee"} + {user?.display_name !== "" ? `${user?.display_name}` : "No assignee"}
{user.count} diff --git a/web/components/core/sidebar/sidebar-progress-stats.tsx b/web/components/core/sidebar/sidebar-progress-stats.tsx index 157fd2c79..6ff3d3f1e 100644 --- a/web/components/core/sidebar/sidebar-progress-stats.tsx +++ b/web/components/core/sidebar/sidebar-progress-stats.tsx @@ -137,8 +137,8 @@ export const SidebarProgressStats: React.FC = ({ key={assignee.assignee_id} title={
- - {assignee.display_name} + + {assignee?.display_name ?? ""}
} completed={assignee.completed_issues} diff --git a/web/components/cycles/active-cycle-stats.tsx b/web/components/cycles/active-cycle-stats.tsx index 7d935c347..0cf7449ae 100644 --- a/web/components/cycles/active-cycle-stats.tsx +++ b/web/components/cycles/active-cycle-stats.tsx @@ -82,7 +82,7 @@ export const ActiveCycleProgressStats: React.FC = ({ cycle }) => {
- {assignee.display_name} + {assignee?.display_name ?? ""}
} completed={assignee.completed_issues} diff --git a/web/components/cycles/cycle-mobile-header.tsx b/web/components/cycles/cycle-mobile-header.tsx index 942b5832b..add78943c 100644 --- a/web/components/cycles/cycle-mobile-header.tsx +++ b/web/components/cycles/cycle-mobile-header.tsx @@ -21,11 +21,7 @@ export const CycleMobileHeader = () => { { key: "calendar", title: "Calendar", icon: Calendar }, ]; - const { workspaceSlug, projectId, cycleId } = router.query as { - workspaceSlug: string; - projectId: string; - cycleId: string; - }; + const { workspaceSlug, projectId, cycleId } = router.query; const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined; // store hooks const { @@ -35,8 +31,14 @@ export const CycleMobileHeader = () => { const handleLayoutChange = useCallback( (layout: TIssueLayouts) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, cycleId); + if (!workspaceSlug || !projectId || !cycleId) return; + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.DISPLAY_FILTERS, + { layout: layout }, + cycleId.toString() + ); }, [workspaceSlug, projectId, cycleId, updateFilters] ); @@ -49,7 +51,7 @@ export const CycleMobileHeader = () => { const handleFiltersUpdate = useCallback( (key: keyof IIssueFilterOptions, value: string | string[]) => { - if (!workspaceSlug || !projectId) return; + if (!workspaceSlug || !projectId || !cycleId) return; const newValues = issueFilters?.filters?.[key] ?? []; if (Array.isArray(value)) { @@ -61,23 +63,41 @@ export const CycleMobileHeader = () => { else newValues.push(value); } - updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, cycleId); + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.FILTERS, + { [key]: newValues }, + cycleId.toString() + ); }, [workspaceSlug, projectId, cycleId, issueFilters, updateFilters] ); const handleDisplayFilters = useCallback( (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, cycleId); + if (!workspaceSlug || !projectId || !cycleId) return; + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.DISPLAY_FILTERS, + updatedDisplayFilter, + cycleId.toString() + ); }, [workspaceSlug, projectId, cycleId, updateFilters] ); const handleDisplayProperties = useCallback( (property: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, cycleId); + if (!workspaceSlug || !projectId || !cycleId) return; + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.DISPLAY_PROPERTIES, + property, + cycleId.toString() + ); }, [workspaceSlug, projectId, cycleId, updateFilters] ); diff --git a/web/components/dashboard/widgets/recent-activity.tsx b/web/components/dashboard/widgets/recent-activity.tsx index 56056bce0..6e5c61355 100644 --- a/web/components/dashboard/widgets/recent-activity.tsx +++ b/web/components/dashboard/widgets/recent-activity.tsx @@ -71,7 +71,7 @@ export const RecentActivityWidget: React.FC = observer((props) => {

- {currentUser?.id === activity.actor_detail.id ? "You" : activity.actor_detail.display_name}{" "} + {currentUser?.id === activity.actor_detail.id ? "You" : activity.actor_detail?.display_name}{" "} {activity.field ? ( diff --git a/web/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx b/web/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx index cfe7dd5ca..1e796eea2 100644 --- a/web/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx +++ b/web/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx @@ -100,14 +100,14 @@ export const CollaboratorsList: React.FC = (props) => { updateIsLoading?.(false); updateTotalPages(widgetStats.total_pages); - updateResultsCount(widgetStats.results.length); + updateResultsCount(widgetStats.results?.length); }, [updateIsLoading, updateResultsCount, updateTotalPages, widgetStats]); - if (!widgetStats) return ; + if (!widgetStats || !widgetStats?.results) return ; return ( <> - {widgetStats?.results.map((user) => ( + {widgetStats?.results?.map((user) => ( { const { workspaceSlug, projectId, moduleId } = router.query; // store hooks const { - issuesFilter: { issueFilters, updateFilters }, + issuesFilter: { issueFilters }, } = useIssues(EIssuesStoreType.MODULE); + const { updateFilters } = useIssuesActions(EIssuesStoreType.MODULE); const { projectModuleIds, getModuleById } = useModule(); const { commandPalette: { toggleCreateIssueModal }, @@ -95,21 +97,15 @@ export const ModuleIssuesHeader: React.FC = observer(() => { const handleLayoutChange = useCallback( (layout: TIssueLayouts) => { - if (!workspaceSlug || !projectId) return; - updateFilters( - workspaceSlug.toString(), - projectId.toString(), - EIssueFilterType.DISPLAY_FILTERS, - { layout: layout }, - moduleId?.toString() - ); + if (!projectId) return; + updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, { layout: layout }); }, - [workspaceSlug, projectId, moduleId, updateFilters] + [projectId, moduleId, updateFilters] ); const handleFiltersUpdate = useCallback( (key: keyof IIssueFilterOptions, value: string | string[]) => { - if (!workspaceSlug || !projectId) return; + if (!projectId) return; const newValues = issueFilters?.filters?.[key] ?? []; if (Array.isArray(value)) { @@ -121,43 +117,25 @@ export const ModuleIssuesHeader: React.FC = observer(() => { else newValues.push(value); } - updateFilters( - workspaceSlug.toString(), - projectId.toString(), - EIssueFilterType.FILTERS, - { [key]: newValues }, - moduleId?.toString() - ); + updateFilters(projectId.toString(), EIssueFilterType.FILTERS, { [key]: newValues }); }, - [workspaceSlug, projectId, moduleId, issueFilters, updateFilters] + [projectId, moduleId, issueFilters, updateFilters] ); const handleDisplayFilters = useCallback( (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters( - workspaceSlug.toString(), - projectId.toString(), - EIssueFilterType.DISPLAY_FILTERS, - updatedDisplayFilter, - moduleId?.toString() - ); + if (!projectId) return; + updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter); }, - [workspaceSlug, projectId, moduleId, updateFilters] + [projectId, moduleId, updateFilters] ); const handleDisplayProperties = useCallback( (property: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters( - workspaceSlug.toString(), - projectId.toString(), - EIssueFilterType.DISPLAY_PROPERTIES, - property, - moduleId?.toString() - ); + if (!projectId) return; + updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_PROPERTIES, property); }, - [workspaceSlug, projectId, moduleId, updateFilters] + [projectId, moduleId, updateFilters] ); // derived values diff --git a/web/components/headers/project-view-issues.tsx b/web/components/headers/project-view-issues.tsx index 4abc3edf9..ab3959716 100644 --- a/web/components/headers/project-view-issues.tsx +++ b/web/components/headers/project-view-issues.tsx @@ -33,11 +33,7 @@ import { ProjectLogo } from "components/project"; export const ProjectViewIssuesHeader: React.FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug, projectId, viewId } = router.query as { - workspaceSlug: string; - projectId: string; - viewId: string; - }; + const { workspaceSlug, projectId, viewId } = router.query; // store hooks const { issuesFilter: { issueFilters, updateFilters }, @@ -61,15 +57,21 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { const handleLayoutChange = useCallback( (layout: TIssueLayouts) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, viewId); + if (!workspaceSlug || !projectId || !viewId) return; + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.DISPLAY_FILTERS, + { layout: layout }, + viewId.toString() + ); }, [workspaceSlug, projectId, viewId, updateFilters] ); const handleFiltersUpdate = useCallback( (key: keyof IIssueFilterOptions, value: string | string[]) => { - if (!workspaceSlug || !projectId) return; + if (!workspaceSlug || !projectId || !viewId) return; const newValues = issueFilters?.filters?.[key] ?? []; if (Array.isArray(value)) { @@ -81,23 +83,41 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { else newValues.push(value); } - updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, viewId); + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.FILTERS, + { [key]: newValues }, + viewId.toString() + ); }, [workspaceSlug, projectId, viewId, issueFilters, updateFilters] ); const handleDisplayFilters = useCallback( (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, viewId); + if (!workspaceSlug || !projectId || !viewId) return; + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.DISPLAY_FILTERS, + updatedDisplayFilter, + viewId.toString() + ); }, [workspaceSlug, projectId, viewId, updateFilters] ); const handleDisplayProperties = useCallback( (property: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, viewId); + if (!workspaceSlug || !projectId || !viewId) return; + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.DISPLAY_PROPERTIES, + property, + viewId.toString() + ); }, [workspaceSlug, projectId, viewId, updateFilters] ); diff --git a/web/components/integration/github/single-user-select.tsx b/web/components/integration/github/single-user-select.tsx index 24bd677d0..2a323e72e 100644 --- a/web/components/integration/github/single-user-select.tsx +++ b/web/components/integration/github/single-user-select.tsx @@ -44,16 +44,27 @@ export const SingleUserSelect: React.FC = ({ collaborator, index, users, workspaceSlug ? () => workspaceService.fetchWorkspaceMembers(workspaceSlug.toString()) : null ); - const options = members?.map((member) => ({ - value: member.member.display_name, - query: member.member.display_name ?? "", - content: ( -

- - {member.member.display_name} -
- ), - })); + const options = members + ?.map((member) => { + if (!member?.member) return; + return { + value: member.member?.display_name, + query: member.member?.display_name ?? "", + content: ( +
+ + {member.member?.display_name} +
+ ), + }; + }) + .filter((member) => !!member) as + | { + value: string; + query: string; + content: JSX.Element; + }[] + | undefined; return (
diff --git a/web/components/integration/jira/import-users.tsx b/web/components/integration/jira/import-users.tsx index 584ba9fee..f5d1221ae 100644 --- a/web/components/integration/jira/import-users.tsx +++ b/web/components/integration/jira/import-users.tsx @@ -33,16 +33,27 @@ export const JiraImportUsers: FC = () => { workspaceSlug ? () => workspaceService.fetchWorkspaceMembers(workspaceSlug?.toString() ?? "") : null ); - const options = members?.map((member) => ({ - value: member.member.email, - query: member.member.display_name ?? "", - content: ( -
- - {member.member.display_name} -
- ), - })); + const options = members + ?.map((member) => { + if (!member?.member) return; + return { + value: member.member.email, + query: member.member.display_name ?? "", + content: ( +
+ + {member.member.display_name} +
+ ), + }; + }) + .filter((member) => !!member) as + | { + value: string; + query: string; + content: JSX.Element; + }[] + | undefined; return (
diff --git a/web/components/integration/single-import.tsx b/web/components/integration/single-import.tsx index 6d1d925e9..5d83a92c9 100644 --- a/web/components/integration/single-import.tsx +++ b/web/components/integration/single-import.tsx @@ -40,7 +40,7 @@ export const SingleImport: React.FC = ({ service, refreshing, handleDelet
{renderFormattedDate(service.created_at)}| - Imported by {service.initiated_by_detail.display_name} + Imported by {service.initiated_by_detail?.display_name}
diff --git a/web/components/issues/issue-detail/root.tsx b/web/components/issues/issue-detail/root.tsx index f714a5279..5e56170a8 100644 --- a/web/components/issues/issue-detail/root.tsx +++ b/web/components/issues/issue-detail/root.tsx @@ -217,10 +217,10 @@ export const IssueDetailRoot: FC = observer((props) => { message: () => "Cycle remove from issue failed", }, }); - const response = await removeFromCyclePromise; + await removeFromCyclePromise; captureIssueEvent({ eventName: ISSUE_UPDATED, - payload: { ...response, state: "SUCCESS", element: "Issue detail page" }, + payload: { issueId, state: "SUCCESS", element: "Issue detail page" }, updates: { changed_property: "cycle_id", change_details: "", diff --git a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx index ab47a7399..a36b8cc47 100644 --- a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx +++ b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx @@ -1,34 +1,30 @@ -import { FC, useCallback } from "react"; +import { FC } from "react"; import { DragDropContext, DropResult } from "@hello-pangea/dnd"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // components import { TOAST_TYPE, setToast } from "@plane/ui"; import { CalendarChart } from "components/issues"; +// hooks +import { useIssues, useUser } from "hooks/store"; +import { useIssuesActions } from "hooks/use-issues-actions"; // ui // types -import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle"; -import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module"; -import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project"; -import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views"; -import { TGroupedIssues, TIssue } from "@plane/types"; +import { TGroupedIssues } from "@plane/types"; +import { EIssuesStoreType } from "constants/issue"; import { IQuickActionProps } from "../list/list-view-types"; -import { EIssueActions } from "../types"; import { handleDragDrop } from "./utils"; -import { useIssues, useUser } from "hooks/store"; import { EUserProjectRoles } from "constants/project"; +type CalendarStoreType = + | EIssuesStoreType.PROJECT + | EIssuesStoreType.MODULE + | EIssuesStoreType.CYCLE + | EIssuesStoreType.PROJECT_VIEW; + interface IBaseCalendarRoot { - issueStore: IProjectIssues | IModuleIssues | ICycleIssues | IProjectViewIssues; - issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; QuickActions: FC; - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; - [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; - [EIssueActions.RESTORE]?: (issue: TIssue) => Promise; - }; + storeType: CalendarStoreType; addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; isCompletedCycle?: boolean; @@ -36,10 +32,8 @@ interface IBaseCalendarRoot { export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { const { - issueStore, - issuesFilterStore, QuickActions, - issueActions, + storeType, addIssuesToView, viewId, isCompletedCycle = false, @@ -50,16 +44,18 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { const { workspaceSlug, projectId } = router.query; // hooks - const { issueMap } = useIssues(); const { membership: { currentProjectRole }, } = useUser(); + const { issues, issuesFilter, issueMap } = useIssues(storeType); + const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue, updateFilters } = + useIssuesActions(storeType); const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - const displayFilters = issuesFilterStore.issueFilters?.displayFilters; + const displayFilters = issuesFilter.issueFilters?.displayFilters; - const groupedIssueIds = (issueStore.groupedIssueIds ?? {}) as TGroupedIssues; + const groupedIssueIds = (issues.groupedIssueIds ?? {}) as TGroupedIssues; const onDragEnd = async (result: DropResult) => { if (!result) return; @@ -76,10 +72,9 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { result.destination, workspaceSlug?.toString(), projectId?.toString(), - issueStore, issueMap, groupedIssueIds, - viewId + updateIssue ).catch((err) => { setToast({ title: "Error", @@ -90,21 +85,12 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { } }; - const handleIssues = useCallback( - async (date: string, issue: TIssue, action: EIssueActions) => { - if (issueActions[action]) { - await issueActions[action]!(issue); - } - }, - [issueActions] - ); - return ( <>
{ handleIssues(issue.target_date ?? "", issue, EIssueActions.DELETE)} - handleUpdate={ - issueActions[EIssueActions.UPDATE] - ? async (data) => handleIssues(issue.target_date ?? "", data, EIssueActions.UPDATE) - : undefined - } - handleRemoveFromView={ - issueActions[EIssueActions.REMOVE] - ? async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.REMOVE) - : undefined - } - handleArchive={ - issueActions[EIssueActions.ARCHIVE] - ? async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.ARCHIVE) - : undefined - } - handleRestore={ - issueActions[EIssueActions.RESTORE] - ? async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.RESTORE) - : undefined + handleDelete={async () => removeIssue(issue.project_id, issue.id)} + handleUpdate={async (data) => updateIssue && updateIssue(issue.project_id, issue.id, data)} + handleRemoveFromView={async () => + removeIssueFromView && removeIssueFromView(issue.project_id, issue.id) } + handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)} + handleRestore={async () => restoreIssue && restoreIssue(issue.project_id, issue.id)} readOnly={!isEditingAllowed || isCompletedCycle} /> )} addIssuesToView={addIssuesToView} - quickAddCallback={issueStore.quickAddIssue} + quickAddCallback={issues.quickAddIssue} viewId={viewId} readOnly={!isEditingAllowed || isCompletedCycle} + updateFilters={updateFilters} />
diff --git a/web/components/issues/issue-layouts/calendar/calendar.tsx b/web/components/issues/issue-layouts/calendar/calendar.tsx index 308393267..823866d98 100644 --- a/web/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/components/issues/issue-layouts/calendar/calendar.tsx @@ -5,8 +5,10 @@ import { observer } from "mobx-react-lite"; import { Spinner } from "@plane/ui"; import { CalendarHeader, CalendarWeekDays, CalendarWeekHeader } from "components/issues"; // types +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TGroupedIssues, TIssue, TIssueKanbanFilters, TIssueMap } from "@plane/types"; +import { ICalendarWeek } from "./types"; // constants -import { EIssuesStoreType } from "constants/issue"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; import { useIssues, useUser } from "hooks/store"; import { useCalendarView } from "hooks/store/use-calendar-view"; @@ -14,8 +16,6 @@ import { ICycleIssuesFilter } from "store/issue/cycle"; import { IModuleIssuesFilter } from "store/issue/module"; import { IProjectIssuesFilter } from "store/issue/project"; import { IProjectViewIssuesFilter } from "store/issue/project-views"; -import { TGroupedIssues, TIssue, TIssueMap } from "@plane/types"; -import { ICalendarWeek } from "./types"; type Props = { issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; @@ -33,6 +33,11 @@ type Props = { addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; readOnly?: boolean; + updateFilters?: ( + projectId: string, + filterType: EIssueFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters + ) => Promise; }; export const CalendarChart: React.FC = observer((props) => { @@ -46,6 +51,7 @@ export const CalendarChart: React.FC = observer((props) => { quickAddCallback, addIssuesToView, viewId, + updateFilters, readOnly = false, } = props; // store hooks @@ -74,7 +80,7 @@ export const CalendarChart: React.FC = observer((props) => { return ( <>
- +
diff --git a/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx b/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx index 6d00253da..d483ebe91 100644 --- a/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx +++ b/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx @@ -9,6 +9,7 @@ import { Popover, Transition } from "@headlessui/react"; import { Check, ChevronUp } from "lucide-react"; import { ToggleSwitch } from "@plane/ui"; // types +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TCalendarLayouts, TIssueKanbanFilters } from "@plane/types"; // constants import { CALENDAR_LAYOUTS } from "constants/calendar"; import { EIssueFilterType } from "constants/issue"; @@ -17,18 +18,21 @@ import { ICycleIssuesFilter } from "store/issue/cycle"; import { IModuleIssuesFilter } from "store/issue/module"; import { IProjectIssuesFilter } from "store/issue/project"; import { IProjectViewIssuesFilter } from "store/issue/project-views"; -import { TCalendarLayouts } from "@plane/types"; interface ICalendarHeader { issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; - viewId?: string; + updateFilters?: ( + projectId: string, + filterType: EIssueFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters + ) => Promise; } export const CalendarOptionsDropdown: React.FC = observer((props) => { - const { issuesFilterStore, viewId } = props; + const { issuesFilterStore, updateFilters } = props; const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { projectId } = router.query; const issueCalendarView = useCalendarView(); @@ -51,20 +55,14 @@ export const CalendarOptionsDropdown: React.FC = observer((prop const showWeekends = issuesFilterStore.issueFilters?.displayFilters?.calendar?.show_weekends ?? false; const handleLayoutChange = (layout: TCalendarLayouts) => { - if (!workspaceSlug || !projectId) return; + if (!projectId || !updateFilters) return; - issuesFilterStore.updateFilters( - workspaceSlug.toString(), - projectId.toString(), - EIssueFilterType.DISPLAY_FILTERS, - { - calendar: { - ...issuesFilterStore.issueFilters?.displayFilters?.calendar, - layout, - }, + updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, { + calendar: { + ...issuesFilterStore.issueFilters?.displayFilters?.calendar, + layout, }, - viewId - ); + }); issueCalendarView.updateCalendarPayload( layout === "month" @@ -76,20 +74,14 @@ export const CalendarOptionsDropdown: React.FC = observer((prop const handleToggleWeekends = () => { const showWeekends = issuesFilterStore.issueFilters?.displayFilters?.calendar?.show_weekends ?? false; - if (!workspaceSlug || !projectId) return; + if (!projectId || !updateFilters) return; - issuesFilterStore.updateFilters( - workspaceSlug.toString(), - projectId.toString(), - EIssueFilterType.DISPLAY_FILTERS, - { - calendar: { - ...issuesFilterStore.issueFilters?.displayFilters?.calendar, - show_weekends: !showWeekends, - }, + updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, { + calendar: { + ...issuesFilterStore.issueFilters?.displayFilters?.calendar, + show_weekends: !showWeekends, }, - viewId - ); + }); }; return ( diff --git a/web/components/issues/issue-layouts/calendar/header.tsx b/web/components/issues/issue-layouts/calendar/header.tsx index 6129e451b..aa055534d 100644 --- a/web/components/issues/issue-layouts/calendar/header.tsx +++ b/web/components/issues/issue-layouts/calendar/header.tsx @@ -9,14 +9,25 @@ import { ICycleIssuesFilter } from "store/issue/cycle"; import { IModuleIssuesFilter } from "store/issue/module"; import { IProjectIssuesFilter } from "store/issue/project"; import { IProjectViewIssuesFilter } from "store/issue/project-views"; +import { EIssueFilterType } from "constants/issue"; +import { + IIssueDisplayFilterOptions, + IIssueDisplayProperties, + IIssueFilterOptions, + TIssueKanbanFilters, +} from "@plane/types"; interface ICalendarHeader { issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; - viewId?: string; + updateFilters?: ( + projectId: string, + filterType: EIssueFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters + ) => Promise; } export const CalendarHeader: React.FC = observer((props) => { - const { issuesFilterStore, viewId } = props; + const { issuesFilterStore, updateFilters } = props; const issueCalendarView = useCalendarView(); @@ -101,7 +112,7 @@ export const CalendarHeader: React.FC = observer((props) => { > Today - +
); diff --git a/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx b/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx index 80a21838d..128c84ba5 100644 --- a/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from "react"; +import { useCallback } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; //hooks @@ -7,40 +7,15 @@ import { useCycle, useIssues } from "hooks/store"; import { CycleIssueQuickActions } from "components/issues"; import { BaseCalendarRoot } from "../base-calendar-root"; // types -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../../types"; // constants import { EIssuesStoreType } from "constants/issue"; export const CycleCalendarLayout: React.FC = observer(() => { - const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); const { currentProjectCompletedCycleIds } = useCycle(); - const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, cycleId.toString()); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId || !projectId) return; - await issues.removeIssueFromCycle(workspaceSlug.toString(), issue.project_id, cycleId.toString(), issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); - }, - }), - [issues, workspaceSlug, cycleId, projectId] - ); + const { issues } = useIssues(EIssuesStoreType.CYCLE); if (!cycleId) return null; @@ -57,13 +32,11 @@ export const CycleCalendarLayout: React.FC = observer(() => { return ( ); }); diff --git a/web/components/issues/issue-layouts/calendar/roots/module-root.tsx b/web/components/issues/issue-layouts/calendar/roots/module-root.tsx index b2e2769ee..f6080630f 100644 --- a/web/components/issues/issue-layouts/calendar/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/module-root.tsx @@ -1,47 +1,22 @@ -import { useCallback, useMemo } from "react"; +import { useCallback } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks -import { useIssues } from "hooks/store"; // components import { ModuleIssueQuickActions } from "components/issues"; import { BaseCalendarRoot } from "../base-calendar-root"; // types -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../../types"; // constants import { EIssuesStoreType } from "constants/issue"; +import { useIssues } from "hooks/store"; export const ModuleCalendarLayout: React.FC = observer(() => { - const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); const router = useRouter(); - const { workspaceSlug, projectId, moduleId } = router.query as { - workspaceSlug: string; - projectId: string; - moduleId: string; - }; + const { workspaceSlug, projectId, moduleId } = router.query ; - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - await issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue, moduleId); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - await issues.removeIssue(workspaceSlug, issue.project_id, issue.id, moduleId); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - await issues.removeIssueFromModule(workspaceSlug, issue.project_id, moduleId, issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - await issues.archiveIssue(workspaceSlug, issue.project_id, issue.id, moduleId); - }, - }), - [issues, workspaceSlug, moduleId] - ); + const {issues} = useIssues(EIssuesStoreType.MODULE) + + if (!moduleId) return null; const addIssuesToView = useCallback( (issueIds: string[]) => { @@ -53,12 +28,10 @@ export const ModuleCalendarLayout: React.FC = observer(() => { return ( ); }); diff --git a/web/components/issues/issue-layouts/calendar/roots/project-root.tsx b/web/components/issues/issue-layouts/calendar/roots/project-root.tsx index f8933a227..ad0cffe33 100644 --- a/web/components/issues/issue-layouts/calendar/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/project-root.tsx @@ -1,48 +1,10 @@ -import { useMemo } from "react"; import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; // hooks import { ProjectIssueQuickActions } from "components/issues"; import { EIssuesStoreType } from "constants/issue"; -import { useIssues } from "hooks/store"; // components -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../../types"; import { BaseCalendarRoot } from "../base-calendar-root"; -export const CalendarLayout: React.FC = observer(() => { - const router = useRouter(); - const { workspaceSlug } = router.query; - - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id); - }, - }), - [issues, workspaceSlug] - ); - - return ( - - ); -}); +export const CalendarLayout: React.FC = observer(() => ( + +)); diff --git a/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx b/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx index b50efd6c7..ff1b654e5 100644 --- a/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx @@ -3,38 +3,21 @@ import { useRouter } from "next/router"; // hooks import { ProjectIssueQuickActions } from "components/issues"; import { EIssuesStoreType } from "constants/issue"; -import { useIssues } from "hooks/store"; // components // types -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../../types"; import { BaseCalendarRoot } from "../base-calendar-root"; // constants -export interface IViewCalendarLayout { - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; - [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; - }; -} - -export const ProjectViewCalendarLayout: React.FC = observer((props) => { - const { issueActions } = props; - // store - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW); +export const ProjectViewCalendarLayout: React.FC = observer(() => { // router const router = useRouter(); const { viewId } = router.query; return ( ); }); diff --git a/web/components/issues/issue-layouts/calendar/utils.ts b/web/components/issues/issue-layouts/calendar/utils.ts index 82d9ce0ce..fd96ff647 100644 --- a/web/components/issues/issue-layouts/calendar/utils.ts +++ b/web/components/issues/issue-layouts/calendar/utils.ts @@ -1,21 +1,16 @@ import { DraggableLocation } from "@hello-pangea/dnd"; -import { ICycleIssues } from "store/issue/cycle"; -import { IModuleIssues } from "store/issue/module"; -import { IProjectIssues } from "store/issue/project"; -import { IProjectViewIssues } from "store/issue/project-views"; -import { TGroupedIssues, IIssueMap } from "@plane/types"; +import { TGroupedIssues, IIssueMap, TIssue } from "@plane/types"; export const handleDragDrop = async ( source: DraggableLocation, destination: DraggableLocation, workspaceSlug: string | undefined, projectId: string | undefined, - store: IProjectIssues | IModuleIssues | ICycleIssues | IProjectViewIssues, issueMap: IIssueMap, issueWithIds: TGroupedIssues, - viewId: string | null = null // it can be moduleId, cycleId + updateIssue?: (projectId: string, issueId: string, data: Partial) => Promise ) => { - if (!issueMap || !issueWithIds || !workspaceSlug || !projectId) return; + if (!issueMap || !issueWithIds || !workspaceSlug || !projectId || !updateIssue) return; const sourceColumnId = source?.droppableId || null; const destinationColumnId = destination?.droppableId || null; @@ -31,12 +26,11 @@ export const handleDragDrop = async ( const [removed] = sourceIssues.splice(source.index, 1); const removedIssueDetail = issueMap[removed]; - const updateIssue = { + const updatedIssue = { id: removedIssueDetail?.id, target_date: destinationColumnId, }; - if (viewId) return await store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue, viewId); - else return await store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue); + return await updateIssue(projectId, updatedIssue.id, updatedIssue); } }; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx index 57e28240b..6a741b73d 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx @@ -11,11 +11,7 @@ import { IIssueFilterOptions } from "@plane/types"; export const CycleAppliedFiltersRoot: React.FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug, projectId, cycleId } = router.query as { - workspaceSlug: string; - projectId: string; - cycleId: string; - }; + const { workspaceSlug, projectId, cycleId } = router.query; // store hooks const { issuesFilter: { issueFilters, updateFilters }, @@ -37,13 +33,13 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => { if (!workspaceSlug || !projectId || !cycleId) return; if (!value) { updateFilters( - workspaceSlug, - projectId, + workspaceSlug.toString(), + projectId.toString(), EIssueFilterType.FILTERS, { [key]: null, }, - cycleId + cycleId.toString() ); return; } @@ -52,13 +48,13 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => { newValues = newValues.filter((val) => val !== value); updateFilters( - workspaceSlug, - projectId, + workspaceSlug.toString(), + projectId.toString(), EIssueFilterType.FILTERS, { [key]: newValues, }, - cycleId + cycleId.toString() ); }; @@ -68,11 +64,17 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => { Object.keys(userFilters ?? {}).forEach((key) => { newFilters[key as keyof IIssueFilterOptions] = null; }); - updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { ...newFilters }, cycleId); + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.FILTERS, + { ...newFilters }, + cycleId.toString() + ); }; // return if no filters are applied - if (Object.keys(appliedFilters).length === 0) return null; + if (Object.keys(appliedFilters).length === 0 || !workspaceSlug || !projectId) return null; return (
@@ -84,7 +86,11 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => { states={projectStates} /> - +
); }); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx index d2c9ba7ed..b49ddf4d6 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx @@ -11,11 +11,7 @@ import { IIssueFilterOptions } from "@plane/types"; export const ModuleAppliedFiltersRoot: React.FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug, projectId, moduleId } = router.query as { - workspaceSlug: string; - projectId: string; - moduleId: string; - }; + const { workspaceSlug, projectId, moduleId } = router.query; // store hooks const { issuesFilter: { issueFilters, updateFilters }, @@ -36,13 +32,13 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => { if (!workspaceSlug || !projectId || !moduleId) return; if (!value) { updateFilters( - workspaceSlug, - projectId, + workspaceSlug.toString(), + projectId.toString(), EIssueFilterType.FILTERS, { [key]: null, }, - moduleId + moduleId.toString() ); return; } @@ -51,13 +47,13 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => { newValues = newValues.filter((val) => val !== value); updateFilters( - workspaceSlug, - projectId, + workspaceSlug.toString(), + projectId.toString(), EIssueFilterType.FILTERS, { [key]: newValues, }, - moduleId + moduleId.toString() ); }; @@ -67,11 +63,17 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => { Object.keys(userFilters ?? {}).forEach((key) => { newFilters[key as keyof IIssueFilterOptions] = null; }); - updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { ...newFilters }, moduleId); + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.FILTERS, + { ...newFilters }, + moduleId.toString() + ); }; // return if no filters are applied - if (Object.keys(appliedFilters).length === 0) return null; + if (!workspaceSlug || !projectId || Object.keys(appliedFilters).length === 0) return null; return (
@@ -83,7 +85,11 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => { states={projectStates} /> - +
); }); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx index 278e19d65..760d2e7e4 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx @@ -14,11 +14,7 @@ import { IIssueFilterOptions } from "@plane/types"; export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug, projectId, viewId } = router.query as { - workspaceSlug: string; - projectId: string; - viewId: string; - }; + const { workspaceSlug, projectId, viewId } = router.query; // store hooks const { issuesFilter: { issueFilters, updateFilters }, @@ -39,16 +35,16 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { }); const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { - if (!workspaceSlug || !projectId) return; + if (!workspaceSlug || !projectId || !viewId) return; if (!value) { updateFilters( - workspaceSlug, - projectId, + workspaceSlug.toString(), + projectId.toString(), EIssueFilterType.FILTERS, { [key]: null, }, - viewId + viewId.toString() ); return; } @@ -57,23 +53,29 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { newValues = newValues.filter((val) => val !== value); updateFilters( - workspaceSlug, - projectId, + workspaceSlug.toString(), + projectId.toString(), EIssueFilterType.FILTERS, { [key]: newValues, }, - viewId + viewId.toString() ); }; const handleClearAllFilters = () => { - if (!workspaceSlug || !projectId) return; + if (!workspaceSlug || !projectId || !viewId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { newFilters[key as keyof IIssueFilterOptions] = null; }); - updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { ...newFilters }, viewId); + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.FILTERS, + { ...newFilters }, + viewId.toString() + ); }; const areFiltersEqual = isEqual(appliedFilters, viewDetails?.filters); diff --git a/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx b/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx index 1231a31c5..11f52db80 100644 --- a/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx +++ b/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx @@ -7,44 +7,43 @@ import { GanttQuickAddIssueForm, IssueGanttBlock } from "components/issues"; import { EUserProjectRoles } from "constants/project"; import { renderIssueBlocksStructure } from "helpers/issue.helper"; import { useIssues, useUser } from "hooks/store"; +import { useIssuesActions } from "hooks/use-issues-actions"; // components // helpers // types -import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle"; -import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module"; -import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project"; -import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views"; import { TIssue, TUnGroupedIssues } from "@plane/types"; // constants -import { EIssueActions } from "../types"; +import { EIssuesStoreType } from "constants/issue"; +type GanttStoreType = + | EIssuesStoreType.PROJECT + | EIssuesStoreType.MODULE + | EIssuesStoreType.CYCLE + | EIssuesStoreType.PROJECT_VIEW; interface IBaseGanttRoot { - issueFiltersStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; - issueStore: IProjectIssues | IModuleIssues | ICycleIssues | IProjectViewIssues; viewId?: string; - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; - }; + storeType: GanttStoreType; } export const BaseGanttRoot: React.FC = observer((props: IBaseGanttRoot) => { - const { issueFiltersStore, issueStore, viewId } = props; + const { viewId, storeType } = props; // router const router = useRouter(); const { workspaceSlug } = router.query; + + const { issues, issuesFilter } = useIssues(storeType); + const { updateIssue } = useIssuesActions(storeType); // store hooks const { membership: { currentProjectRole }, } = useUser(); const { issueMap } = useIssues(); - const appliedDisplayFilters = issueFiltersStore.issueFilters?.displayFilters; + const appliedDisplayFilters = issuesFilter.issueFilters?.displayFilters; - const issueIds = (issueStore.groupedIssueIds ?? []) as TUnGroupedIssues; - const { enableIssueCreation } = issueStore?.viewFlags || {}; + const issueIds = (issues.groupedIssueIds ?? []) as TUnGroupedIssues; + const { enableIssueCreation } = issues?.viewFlags || {}; - const issues = issueIds.map((id) => issueMap?.[id]); + const issuesArray = issueIds.map((id) => issueMap?.[id]); const updateIssueBlockStructure = async (issue: TIssue, data: IBlockUpdateData) => { if (!workspaceSlug) return; @@ -52,7 +51,7 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan const payload: any = { ...data }; if (data.sort_order) payload.sort_order = data.sort_order.newSortOrder; - await issueStore.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, payload, viewId); + updateIssue && (await updateIssue(issue.project_id, issue.id, payload)); }; const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; @@ -64,7 +63,7 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan border={false} title="Issues" loaderTitle="Issues" - blocks={issues ? renderIssueBlocksStructure(issues as TIssue[]) : null} + blocks={issues ? renderIssueBlocksStructure(issuesArray) : null} blockUpdateHandler={updateIssueBlockStructure} blockToRender={(data: TIssue) => } sidebarToRender={(props) => } @@ -75,7 +74,7 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan enableAddBlock={isAllowed} quickAdd={ enableIssueCreation && isAllowed ? ( - + ) : undefined } showAllBlocks diff --git a/web/components/issues/issue-layouts/gantt/cycle-root.tsx b/web/components/issues/issue-layouts/gantt/cycle-root.tsx index 4d255b64f..923845e7b 100644 --- a/web/components/issues/issue-layouts/gantt/cycle-root.tsx +++ b/web/components/issues/issue-layouts/gantt/cycle-root.tsx @@ -2,47 +2,13 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks import { EIssuesStoreType } from "constants/issue"; -import { useCycle, useIssues } from "hooks/store"; // components -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../types"; import { BaseGanttRoot } from "./base-gantt-root"; export const CycleGanttLayout: React.FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug, cycleId } = router.query; - // store hooks - const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); - const { fetchCycleDetails } = useCycle(); + const { cycleId } = router.query; - const issueActions = { - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, cycleId.toString()); - fetchCycleDetails(workspaceSlug.toString(), issue.project_id, cycleId.toString()); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); - fetchCycleDetails(workspaceSlug.toString(), issue.project_id, cycleId.toString()); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId || !issue.id) return; - - await issues.removeIssueFromCycle(workspaceSlug.toString(), issue.project_id, cycleId.toString(), issue.id); - fetchCycleDetails(workspaceSlug.toString(), issue.project_id, cycleId.toString()); - }, - }; - - return ( - - ); + return ; }); diff --git a/web/components/issues/issue-layouts/gantt/module-root.tsx b/web/components/issues/issue-layouts/gantt/module-root.tsx index 3311b6c6a..e14f1339a 100644 --- a/web/components/issues/issue-layouts/gantt/module-root.tsx +++ b/web/components/issues/issue-layouts/gantt/module-root.tsx @@ -2,47 +2,13 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks import { EIssuesStoreType } from "constants/issue"; -import { useIssues, useModule } from "hooks/store"; // components -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../types"; import { BaseGanttRoot } from "./base-gantt-root"; export const ModuleGanttLayout: React.FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug, moduleId } = router.query; - // store hooks - const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); - const { fetchModuleDetails } = useModule(); + const { moduleId } = router.query; - const issueActions = { - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, moduleId.toString()); - fetchModuleDetails(workspaceSlug.toString(), issue.project_id, moduleId.toString()); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, moduleId.toString()); - fetchModuleDetails(workspaceSlug.toString(), issue.project_id, moduleId.toString()); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId || !issue.id) return; - - await issues.removeIssueFromModule(workspaceSlug.toString(), issue.project_id, moduleId.toString(), issue.id); - fetchModuleDetails(workspaceSlug.toString(), issue.project_id, moduleId.toString()); - }, - }; - - return ( - - ); + return ; }); diff --git a/web/components/issues/issue-layouts/gantt/project-root.tsx b/web/components/issues/issue-layouts/gantt/project-root.tsx index 1f9e560d3..90fcca145 100644 --- a/web/components/issues/issue-layouts/gantt/project-root.tsx +++ b/web/components/issues/issue-layouts/gantt/project-root.tsx @@ -1,33 +1,8 @@ import React from "react"; import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; // hooks import { EIssuesStoreType } from "constants/issue"; -import { useIssues } from "hooks/store"; // components -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../types"; import { BaseGanttRoot } from "./base-gantt-root"; -export const GanttLayout: React.FC = observer(() => { - // router - const router = useRouter(); - const { workspaceSlug } = router.query; - // store hooks - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT); - - const issueActions = { - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id); - }, - }; - - return ; -}); +export const GanttLayout: React.FC = observer(() =>( )); diff --git a/web/components/issues/issue-layouts/gantt/project-view-root.tsx b/web/components/issues/issue-layouts/gantt/project-view-root.tsx index cda2a1e53..80d5e047b 100644 --- a/web/components/issues/issue-layouts/gantt/project-view-root.tsx +++ b/web/components/issues/issue-layouts/gantt/project-view-root.tsx @@ -2,36 +2,15 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks import { EIssuesStoreType } from "constants/issue"; -import { useIssues } from "hooks/store"; // components -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../types"; import { BaseGanttRoot } from "./base-gantt-root"; // constants // types -export interface IViewGanttLayout { - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; - }; -} - -export const ProjectViewGanttLayout: React.FC = observer((props) => { - const { issueActions } = props; - // store - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW); +export const ProjectViewGanttLayout: React.FC = observer(() => { // router const router = useRouter(); const { viewId } = router.query; - return ( - - ); + return ; }); diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx index f4f204436..0a492b5f7 100644 --- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -6,45 +6,31 @@ import { useRouter } from "next/router"; import { Spinner, TOAST_TYPE, setToast } from "@plane/ui"; import { DeleteIssueModal } from "components/issues"; import { ISSUE_DELETED } from "constants/event-tracker"; -import { EIssueFilterType, TCreateModalStoreTypes } from "constants/issue"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; import { useEventTracker, useIssues, useUser } from "hooks/store"; +import { useIssuesActions } from "hooks/use-issues-actions"; // ui // types -import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle"; -import { IDraftIssues, IDraftIssuesFilter } from "store/issue/draft"; -import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module"; -import { IProfileIssues, IProfileIssuesFilter } from "store/issue/profile"; -import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project"; -import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views"; import { TIssue } from "@plane/types"; import { IQuickActionProps } from "../list/list-view-types"; -import { EIssueActions } from "../types"; //components import { KanBan } from "./default"; import { KanBanSwimLanes } from "./swimlanes"; import { handleDragDrop } from "./utils"; +export type KanbanStoreType = + | EIssuesStoreType.PROJECT + | EIssuesStoreType.MODULE + | EIssuesStoreType.CYCLE + | EIssuesStoreType.PROJECT_VIEW + | EIssuesStoreType.DRAFT + | EIssuesStoreType.PROFILE; export interface IBaseKanBanLayout { - issues: IProjectIssues | ICycleIssues | IDraftIssues | IModuleIssues | IProjectViewIssues | IProfileIssues; - issuesFilter: - | IProjectIssuesFilter - | IModuleIssuesFilter - | ICycleIssuesFilter - | IDraftIssuesFilter - | IProjectViewIssuesFilter - | IProfileIssuesFilter; QuickActions: FC; - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; - [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; - [EIssueActions.RESTORE]?: (issue: TIssue) => Promise; - }; showLoader?: boolean; viewId?: string; - storeType?: TCreateModalStoreTypes; + storeType: KanbanStoreType; addIssuesToView?: (issueIds: string[]) => Promise; canEditPropertiesBasedOnProject?: (projectId: string) => boolean; isCompletedCycle?: boolean; @@ -58,10 +44,7 @@ type KanbanDragState = { export const BaseKanBanRoot: React.FC = observer((props: IBaseKanBanLayout) => { const { - issues, - issuesFilter, QuickActions, - issueActions, showLoader, viewId, storeType, @@ -77,7 +60,9 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas membership: { currentProjectRole }, } = useUser(); const { captureIssueEvent } = useEventTracker(); - const { issueMap } = useIssues(); + const { issueMap, issuesFilter, issues } = useIssues(storeType); + const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue, updateFilters } = + useIssuesActions(storeType); const issueIds = issues?.groupedIssueIds || []; @@ -148,12 +133,12 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas result.destination, workspaceSlug?.toString(), projectId?.toString(), - issues, sub_group_by, group_by, issueMap, issueIds, - viewId + updateIssue, + removeIssue ).catch((err) => { setToast({ title: "Error", @@ -165,55 +150,39 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas } }; - const handleIssues = useCallback( - async (issue: TIssue, action: EIssueActions) => { - if (issueActions[action]) { - await issueActions[action]!(issue); - } - }, - [issueActions] - ); - const renderQuickActions = useCallback( (issue: TIssue, customActionButton?: React.ReactElement) => ( handleIssues(issue, EIssueActions.DELETE)} - handleUpdate={ - issueActions[EIssueActions.UPDATE] ? async (data) => handleIssues(data, EIssueActions.UPDATE) : undefined - } - handleRemoveFromView={ - issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined - } - handleArchive={ - issueActions[EIssueActions.ARCHIVE] ? async () => handleIssues(issue, EIssueActions.ARCHIVE) : undefined - } - handleRestore={ - issueActions[EIssueActions.RESTORE] ? async () => handleIssues(issue, EIssueActions.RESTORE) : undefined - } + handleDelete={async () => removeIssue(issue.project_id, issue.id)} + handleUpdate={async (data) => updateIssue && updateIssue(issue.project_id, issue.id, data)} + handleRemoveFromView={async () => removeIssueFromView && removeIssueFromView(issue.project_id, issue.id)} + handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)} + handleRestore={async () => restoreIssue && restoreIssue(issue.project_id, issue.id)} readOnly={!isEditingAllowed || isCompletedCycle} /> ), // eslint-disable-next-line react-hooks/exhaustive-deps - [issueActions, handleIssues] + [isEditingAllowed, isCompletedCycle, removeIssue, updateIssue, removeIssueFromView, archiveIssue, restoreIssue] ); const handleDeleteIssue = async () => { - if (!handleDragDrop) return; + if (!handleDragDrop || !dragState.draggedIssueId) return; await handleDragDrop( dragState.source, dragState.destination, workspaceSlug?.toString(), projectId?.toString(), - issues, sub_group_by, group_by, issueMap, issueIds, - viewId + updateIssue, + removeIssue ).finally(() => { - handleIssues(issueMap[dragState.draggedIssueId!], EIssueActions.DELETE); + const draggedIssue = issueMap[dragState.draggedIssueId!]; + removeIssue(draggedIssue.project_id, draggedIssue.id); setDeleteIssueModal(false); setDragState({}); captureIssueEvent({ @@ -229,14 +198,12 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas let kanbanFilters = issuesFilter?.issueFilters?.kanbanFilters?.[toggle] || []; if (kanbanFilters.includes(value)) kanbanFilters = kanbanFilters.filter((_value) => _value != value); else kanbanFilters.push(value); - issuesFilter.updateFilters( - workspaceSlug.toString(), + updateFilters( projectId.toString(), EIssueFilterType.KANBAN_FILTERS, { [toggle]: kanbanFilters, - }, - viewId + } ); } }; @@ -294,7 +261,7 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas displayProperties={displayProperties} sub_group_by={sub_group_by} group_by={group_by} - handleIssues={handleIssues} + updateIssue={updateIssue} quickActions={renderQuickActions} handleKanbanFilters={handleKanbanFilters} kanbanFilters={kanbanFilters} diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index 55675dd39..dabecc491 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -12,7 +12,6 @@ import { IssueProperties } from "../properties/all-properties"; import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; // ui // types -import { EIssueActions } from "../types"; // helper interface IssueBlockProps { @@ -23,7 +22,7 @@ interface IssueBlockProps { isDragDisabled: boolean; draggableId: string; index: number; - handleIssues: (issue: TIssue, action: EIssueActions) => void; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue) => React.ReactNode; canEditProperties: (projectId: string | undefined) => boolean; scrollableContainerRef?: MutableRefObject; @@ -34,13 +33,13 @@ interface IssueBlockProps { interface IssueDetailsBlockProps { issue: TIssue; displayProperties: IIssueDisplayProperties | undefined; - handleIssues: (issue: TIssue, action: EIssueActions) => void; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue) => React.ReactNode; isReadOnly: boolean; } const KanbanIssueDetailsBlock: React.FC = observer((props: IssueDetailsBlockProps) => { - const { issue, handleIssues, quickActions, isReadOnly, displayProperties } = props; + const { issue, updateIssue, quickActions, isReadOnly, displayProperties } = props; // hooks const { getProjectIdentifierById } = useProject(); const { @@ -48,10 +47,6 @@ const KanbanIssueDetailsBlock: React.FC = observer((prop } = useApplication(); const { setPeekIssue } = useIssueDetail(); - const updateIssue = async (issueToUpdate: TIssue) => { - if (issueToUpdate) await handleIssues(issueToUpdate, EIssueActions.UPDATE); - }; - const handleIssuePeekOverview = (issue: TIssue) => workspaceSlug && issue && @@ -95,7 +90,7 @@ const KanbanIssueDetailsBlock: React.FC = observer((prop issue={issue} displayProperties={displayProperties} activeLayout="Kanban" - handleIssues={updateIssue} + updateIssue={updateIssue} isReadOnly={isReadOnly} /> @@ -111,7 +106,7 @@ export const KanbanIssueBlock: React.FC = memo((props) => { isDragDisabled, draggableId, index, - handleIssues, + updateIssue, quickActions, canEditProperties, scrollableContainerRef, @@ -159,7 +154,7 @@ export const KanbanIssueBlock: React.FC = memo((props) => { diff --git a/web/components/issues/issue-layouts/kanban/blocks-list.tsx b/web/components/issues/issue-layouts/kanban/blocks-list.tsx index ff1c92873..7a58a4933 100644 --- a/web/components/issues/issue-layouts/kanban/blocks-list.tsx +++ b/web/components/issues/issue-layouts/kanban/blocks-list.tsx @@ -2,7 +2,6 @@ import { MutableRefObject, memo } from "react"; //types import { KanbanIssueBlock } from "components/issues"; import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types"; -import { EIssueActions } from "../types"; // components interface IssueBlocksListProps { @@ -13,7 +12,7 @@ interface IssueBlocksListProps { issueIds: string[]; displayProperties: IIssueDisplayProperties | undefined; isDragDisabled: boolean; - handleIssues: (issue: TIssue, action: EIssueActions) => void; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; canEditProperties: (projectId: string | undefined) => boolean; scrollableContainerRef?: MutableRefObject; @@ -29,7 +28,7 @@ const KanbanIssueBlocksListMemo: React.FC = (props) => { issueIds, displayProperties, isDragDisabled, - handleIssues, + updateIssue, quickActions, canEditProperties, scrollableContainerRef, @@ -54,7 +53,7 @@ const KanbanIssueBlocksListMemo: React.FC = (props) => { issueId={issueId} issuesMap={issuesMap} displayProperties={displayProperties} - handleIssues={handleIssues} + updateIssue={updateIssue} quickActions={quickActions} draggableId={draggableId} index={index} diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx index ece578058..394f5ef18 100644 --- a/web/components/issues/issue-layouts/kanban/default.tsx +++ b/web/components/issues/issue-layouts/kanban/default.tsx @@ -1,7 +1,6 @@ import { MutableRefObject } from "react"; import { observer } from "mobx-react-lite"; // constants -import { TCreateModalStoreTypes } from "constants/issue"; // hooks import { useCycle, @@ -26,11 +25,11 @@ import { TIssueKanbanFilters, } from "@plane/types"; // parent components -import { EIssueActions } from "../types"; import { getGroupByColumns } from "../utils"; // components import { HeaderGroupByCard } from "./headers/group-by-card"; import { KanbanGroup } from "./kanban-group"; +import { KanbanStoreType } from "./base-kanban-root"; export interface IGroupByKanBan { issuesMap: IIssueMap; @@ -40,7 +39,7 @@ export interface IGroupByKanBan { group_by: string | null; sub_group_id: string; isDragDisabled: boolean; - handleIssues: (issue: TIssue, action: EIssueActions) => void; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; kanbanFilters: TIssueKanbanFilters; handleKanbanFilters: any; @@ -53,7 +52,7 @@ export interface IGroupByKanBan { ) => Promise; viewId?: string; disableIssueCreation?: boolean; - storeType?: TCreateModalStoreTypes; + storeType: KanbanStoreType; addIssuesToView?: (issueIds: string[]) => Promise; canEditProperties: (projectId: string | undefined) => boolean; scrollableContainerRef?: MutableRefObject; @@ -70,7 +69,7 @@ const GroupByKanBan: React.FC = observer((props) => { group_by, sub_group_id = "null", isDragDisabled, - handleIssues, + updateIssue, quickActions, kanbanFilters, handleKanbanFilters, @@ -164,7 +163,7 @@ const GroupByKanBan: React.FC = observer((props) => { group_by={group_by} sub_group_id={sub_group_id} isDragDisabled={isDragDisabled} - handleIssues={handleIssues} + updateIssue={updateIssue} quickActions={quickActions} enableQuickIssueCreate={enableQuickIssueCreate} quickAddCallback={quickAddCallback} @@ -190,7 +189,7 @@ export interface IKanBan { sub_group_by: string | null; group_by: string | null; sub_group_id?: string; - handleIssues: (issue: TIssue, action: EIssueActions) => void; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; kanbanFilters: TIssueKanbanFilters; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; @@ -204,7 +203,7 @@ export interface IKanBan { ) => Promise; viewId?: string; disableIssueCreation?: boolean; - storeType?: TCreateModalStoreTypes; + storeType: KanbanStoreType; addIssuesToView?: (issueIds: string[]) => Promise; canEditProperties: (projectId: string | undefined) => boolean; scrollableContainerRef?: MutableRefObject; @@ -219,7 +218,7 @@ export const KanBan: React.FC = observer((props) => { sub_group_by, group_by, sub_group_id = "null", - handleIssues, + updateIssue, quickActions, kanbanFilters, handleKanbanFilters, @@ -246,7 +245,7 @@ export const KanBan: React.FC = observer((props) => { sub_group_by={sub_group_by} sub_group_id={sub_group_id} isDragDisabled={!issueKanBanView?.getCanUserDragDrop(group_by, sub_group_by)} - handleIssues={handleIssues} + updateIssue={updateIssue} quickActions={quickActions} kanbanFilters={kanbanFilters} handleKanbanFilters={handleKanbanFilters} diff --git a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx index b3cc24f28..a14dd5ddc 100644 --- a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx @@ -9,11 +9,11 @@ import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; import { ExistingIssuesListModal } from "components/core"; import { CreateUpdateIssueModal } from "components/issues"; // constants -import { TCreateModalStoreTypes } from "constants/issue"; // hooks import { useEventTracker } from "hooks/store"; // types import { TIssue, ISearchIssueResponse, TIssueKanbanFilters } from "@plane/types"; +import { KanbanStoreType } from "../base-kanban-root"; interface IHeaderGroupByCard { sub_group_by: string | null; @@ -26,7 +26,7 @@ interface IHeaderGroupByCard { handleKanbanFilters: any; issuePayload: Partial; disableIssueCreation?: boolean; - storeType?: TCreateModalStoreTypes; + storeType: KanbanStoreType; addIssuesToView?: (issueIds: string[]) => Promise; } diff --git a/web/components/issues/issue-layouts/kanban/kanban-group.tsx b/web/components/issues/issue-layouts/kanban/kanban-group.tsx index 9d7053216..48e92feba 100644 --- a/web/components/issues/issue-layouts/kanban/kanban-group.tsx +++ b/web/components/issues/issue-layouts/kanban/kanban-group.tsx @@ -12,7 +12,6 @@ import { TSubGroupedIssues, TUnGroupedIssues, } from "@plane/types"; -import { EIssueActions } from "../types"; import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from "."; interface IKanbanGroup { @@ -25,7 +24,7 @@ interface IKanbanGroup { group_by: string | null; sub_group_id: string; isDragDisabled: boolean; - handleIssues: (issue: TIssue, action: EIssueActions) => void; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; enableQuickIssueCreate?: boolean; quickAddCallback?: ( @@ -53,7 +52,7 @@ export const KanbanGroup = (props: IKanbanGroup) => { issueIds, peekIssueId, isDragDisabled, - handleIssues, + updateIssue, quickActions, canEditProperties, enableQuickIssueCreate, @@ -135,7 +134,7 @@ export const KanbanGroup = (props: IKanbanGroup) => { issueIds={(issueIds as TGroupedIssues)?.[groupId] || []} displayProperties={displayProperties} isDragDisabled={isDragDisabled} - handleIssues={handleIssues} + updateIssue={updateIssue} quickActions={quickActions} canEditProperties={canEditProperties} scrollableContainerRef={scrollableContainerRef} diff --git a/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx index c73b12afb..19ac8a1d9 100644 --- a/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo } from "react"; +import React, { useCallback } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks @@ -7,8 +7,6 @@ import { EIssuesStoreType } from "constants/issue"; import { useCycle, useIssues } from "hooks/store"; // ui // types -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../../types"; // components import { BaseKanBanRoot } from "../base-kanban-root"; @@ -19,35 +17,9 @@ export const CycleKanBanLayout: React.FC = observer(() => { const { workspaceSlug, projectId, cycleId } = router.query; // store - const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); + const { issues } = useIssues(EIssuesStoreType.CYCLE); const { currentProjectCompletedCycleIds } = useCycle(); - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, cycleId.toString()); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.removeIssueFromCycle(workspaceSlug.toString(), issue.project_id, cycleId.toString(), issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); - }, - }), - [issues, workspaceSlug, cycleId] - ); - const isCompletedCycle = cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false; @@ -63,9 +35,6 @@ export const CycleKanBanLayout: React.FC = observer(() => { return ( { - const router = useRouter(); - const { workspaceSlug } = router.query; - - // store - const { issues, issuesFilter } = useIssues(EIssuesStoreType.DRAFT); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id); - }, - }), - [issues, workspaceSlug] - ); - - return ( - - ); -}); +export const DraftKanBanLayout: React.FC = observer(() => ( + +)); diff --git a/web/components/issues/issue-layouts/kanban/roots/module-root.tsx b/web/components/issues/issue-layouts/kanban/roots/module-root.tsx index 96cfaceda..eaf96a994 100644 --- a/web/components/issues/issue-layouts/kanban/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/module-root.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from "react"; +import React from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hook @@ -7,9 +7,7 @@ import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components // types -import { TIssue } from "@plane/types"; // constants -import { EIssueActions } from "../../types"; import { BaseKanBanRoot } from "../base-kanban-root"; export interface IModuleKanBanLayout {} @@ -19,39 +17,10 @@ export const ModuleKanBanLayout: React.FC = observer(() => { const { workspaceSlug, projectId, moduleId } = router.query; // store - const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, moduleId.toString()); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, moduleId.toString()); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.removeIssueFromModule(workspaceSlug.toString(), issue.project_id, moduleId.toString(), issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id, moduleId.toString()); - }, - }), - [issues, workspaceSlug, moduleId] - ); + const { issues } = useIssues(EIssuesStoreType.MODULE); return ( { - const router = useRouter(); - const { workspaceSlug, userId } = router.query as { workspaceSlug: string; userId: string }; - - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROFILE); - const { - membership: { currentWorkspaceAllProjectsRole }, - } = useUser(); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !userId) return; - - await issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue, userId); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !userId) return; - - await issues.removeIssue(workspaceSlug, issue.project_id, issue.id, userId); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !userId) return; - - await issues.archiveIssue(workspaceSlug, issue.project_id, issue.id, userId); - }, - }), - [issues, workspaceSlug, userId] - ); + membership: { currentWorkspaceAllProjectsRole }, +} = useUser(); const canEditPropertiesBasedOnProject = (projectId: string) => { const currentProjectRole = currentWorkspaceAllProjectsRole && currentWorkspaceAllProjectsRole[projectId]; @@ -52,9 +22,6 @@ export const ProfileIssuesKanBanLayout: React.FC = observer(() => { return ( { - const router = useRouter(); - const { workspaceSlug } = router.query as { workspaceSlug: string; projectId: string }; - - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.removeIssue(workspaceSlug, issue.project_id, issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.archiveIssue(workspaceSlug, issue.project_id, issue.id); - }, - }), - [issues, workspaceSlug] - ); - - return ( - - ); -}); +export const KanBanLayout: React.FC = observer(() => ( + +)); diff --git a/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx b/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx index 77689e563..c1a07c317 100644 --- a/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx @@ -3,37 +3,19 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks import { EIssuesStoreType } from "constants/issue"; -import { useIssues } from "hooks/store"; // constant // types -import { TIssue } from "@plane/types"; import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; -import { EIssueActions } from "../../types"; // components import { BaseKanBanRoot } from "../base-kanban-root"; -export interface IViewKanBanLayout { - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; - [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; - }; -} - -export const ProjectViewKanBanLayout: React.FC = observer((props) => { - const { issueActions } = props; +export const ProjectViewKanBanLayout: React.FC = observer(() => { // router const router = useRouter(); const { viewId } = router.query; - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW); - return ( void; + storeType: KanbanStoreType; } const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: string) => { @@ -43,6 +43,7 @@ const SubGroupSwimlaneHeader: React.FC = ({ issueIds, sub_group_by, group_by, + storeType, list, kanbanFilters, handleKanbanFilters, @@ -62,6 +63,7 @@ const SubGroupSwimlaneHeader: React.FC = ({ kanbanFilters={kanbanFilters} handleKanbanFilters={handleKanbanFilters} issuePayload={_list.payload} + storeType={storeType} />
))} @@ -73,13 +75,13 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues; showEmptyGroup: boolean; displayProperties: IIssueDisplayProperties | undefined; - handleIssues: (issue: TIssue, action: EIssueActions) => void; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; kanbanFilters: TIssueKanbanFilters; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; isDragStarted?: boolean; disableIssueCreation?: boolean; - storeType?: TCreateModalStoreTypes; + storeType: KanbanStoreType; enableQuickIssueCreate: boolean; canEditProperties: (projectId: string | undefined) => boolean; addIssuesToView?: (issueIds: string[]) => Promise; @@ -99,7 +101,8 @@ const SubGroupSwimlane: React.FC = observer((props) => { sub_group_by, group_by, list, - handleIssues, + storeType, + updateIssue, quickActions, displayProperties, kanbanFilters, @@ -153,7 +156,7 @@ const SubGroupSwimlane: React.FC = observer((props) => { sub_group_by={sub_group_by} group_by={group_by} sub_group_id={_list.id} - handleIssues={handleIssues} + updateIssue={updateIssue} quickActions={quickActions} kanbanFilters={kanbanFilters} handleKanbanFilters={handleKanbanFilters} @@ -165,6 +168,7 @@ const SubGroupSwimlane: React.FC = observer((props) => { viewId={viewId} scrollableContainerRef={scrollableContainerRef} isDragStarted={isDragStarted} + storeType={storeType} />
)} @@ -180,14 +184,14 @@ export interface IKanBanSwimLanes { displayProperties: IIssueDisplayProperties | undefined; sub_group_by: string | null; group_by: string | null; - handleIssues: (issue: TIssue, action: EIssueActions) => void; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; kanbanFilters: TIssueKanbanFilters; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; showEmptyGroup: boolean; isDragStarted?: boolean; disableIssueCreation?: boolean; - storeType?: TCreateModalStoreTypes; + storeType: KanbanStoreType; addIssuesToView?: (issueIds: string[]) => Promise; enableQuickIssueCreate: boolean; quickAddCallback?: ( @@ -208,7 +212,8 @@ export const KanBanSwimLanes: React.FC = observer((props) => { displayProperties, sub_group_by, group_by, - handleIssues, + updateIssue, + storeType, quickActions, kanbanFilters, handleKanbanFilters, @@ -261,6 +266,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { kanbanFilters={kanbanFilters} handleKanbanFilters={handleKanbanFilters} list={groupByList} + storeType={storeType} />
@@ -272,7 +278,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { displayProperties={displayProperties} group_by={group_by} sub_group_by={sub_group_by} - handleIssues={handleIssues} + updateIssue={updateIssue} quickActions={quickActions} kanbanFilters={kanbanFilters} handleKanbanFilters={handleKanbanFilters} @@ -285,6 +291,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { quickAddCallback={quickAddCallback} viewId={viewId} scrollableContainerRef={scrollableContainerRef} + storeType={storeType} /> )}
diff --git a/web/components/issues/issue-layouts/kanban/utils.ts b/web/components/issues/issue-layouts/kanban/utils.ts index 617598524..855f096e6 100644 --- a/web/components/issues/issue-layouts/kanban/utils.ts +++ b/web/components/issues/issue-layouts/kanban/utils.ts @@ -1,12 +1,5 @@ import { DraggableLocation } from "@hello-pangea/dnd"; -import { ICycleIssues } from "store/issue/cycle"; -import { IDraftIssues } from "store/issue/draft"; -import { IModuleIssues } from "store/issue/module"; -import { IProfileIssues } from "store/issue/profile"; -import { IProjectIssues } from "store/issue/project"; -import { IProjectViewIssues } from "store/issue/project-views"; -import { IWorkspaceIssues } from "store/issue/workspace"; -import { TGroupedIssues, IIssueMap, TSubGroupedIssues, TUnGroupedIssues } from "@plane/types"; +import { TGroupedIssues, IIssueMap, TSubGroupedIssues, TUnGroupedIssues, TIssue } from "@plane/types"; const handleSortOrder = (destinationIssues: string[], destinationIndex: number, issueMap: IIssueMap) => { const sortOrderDefaultValue = 65535; @@ -48,24 +41,16 @@ export const handleDragDrop = async ( destination: DraggableLocation | null | undefined, workspaceSlug: string | undefined, projectId: string | undefined, // projectId for all views or user id in profile issues - store: - | IProjectIssues - | ICycleIssues - | IDraftIssues - | IModuleIssues - | IDraftIssues - | IProjectViewIssues - | IProfileIssues - | IWorkspaceIssues, subGroupBy: string | null, groupBy: string | null, issueMap: IIssueMap, issueWithIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined, - viewId: string | null = null // it can be moduleId, cycleId + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined, + removeIssue: (projectId: string, issueId: string) => Promise | undefined ) => { if (!issueMap || !issueWithIds || !source || !destination || !workspaceSlug || !projectId) return; - let updateIssue: any = {}; + let updatedIssue: any = {}; const sourceDroppableId = source?.droppableId; const destinationDroppableId = destination?.droppableId; @@ -100,8 +85,7 @@ export const handleDragDrop = async ( const [removed] = sourceIssues.splice(source.index, 1); if (removed) { - if (viewId) return await store?.removeIssue(workspaceSlug, projectId, removed); //, viewId); - else return await store?.removeIssue(workspaceSlug, projectId, removed); + return await removeIssue(projectId, removed); } } else { //spreading the array to stop changing the original reference @@ -118,14 +102,14 @@ export const handleDragDrop = async ( const [removed] = sourceIssues.splice(source.index, 1); const removedIssueDetail = issueMap[removed]; - updateIssue = { + updatedIssue = { id: removedIssueDetail?.id, project_id: removedIssueDetail?.project_id, }; // for both horizontal and vertical dnd - updateIssue = { - ...updateIssue, + updatedIssue = { + ...updatedIssue, ...handleSortOrder( sourceDroppableId === destinationDroppableId ? sourceIssues : destinationIssues, destination.index, @@ -136,19 +120,19 @@ export const handleDragDrop = async ( if (subGroupBy && sourceSubGroupByColumnId && destinationSubGroupByColumnId) { if (sourceSubGroupByColumnId === destinationSubGroupByColumnId) { if (sourceGroupByColumnId != destinationGroupByColumnId) { - if (groupBy === "state") updateIssue = { ...updateIssue, state_id: destinationGroupByColumnId }; - if (groupBy === "priority") updateIssue = { ...updateIssue, priority: destinationGroupByColumnId }; + if (groupBy === "state") updatedIssue = { ...updatedIssue, state_id: destinationGroupByColumnId }; + if (groupBy === "priority") updatedIssue = { ...updatedIssue, priority: destinationGroupByColumnId }; } } else { if (subGroupBy === "state") - updateIssue = { - ...updateIssue, + updatedIssue = { + ...updatedIssue, state_id: destinationSubGroupByColumnId, priority: destinationGroupByColumnId, }; if (subGroupBy === "priority") - updateIssue = { - ...updateIssue, + updatedIssue = { + ...updatedIssue, state_id: destinationGroupByColumnId, priority: destinationSubGroupByColumnId, }; @@ -156,15 +140,13 @@ export const handleDragDrop = async ( } else { // for horizontal dnd if (sourceColumnId != destinationColumnId) { - if (groupBy === "state") updateIssue = { ...updateIssue, state_id: destinationGroupByColumnId }; - if (groupBy === "priority") updateIssue = { ...updateIssue, priority: destinationGroupByColumnId }; + if (groupBy === "state") updatedIssue = { ...updatedIssue, state_id: destinationGroupByColumnId }; + if (groupBy === "priority") updatedIssue = { ...updatedIssue, priority: destinationGroupByColumnId }; } } - if (updateIssue && updateIssue?.id) { - if (viewId) - return await store?.updateIssue(workspaceSlug, updateIssue.project_id, updateIssue.id, updateIssue, viewId); - else return await store?.updateIssue(workspaceSlug, updateIssue.project_id, updateIssue.id, updateIssue); + if (updatedIssue && updatedIssue?.id) { + return updateIssue && (await updateIssue(updatedIssue.project_id, updatedIssue.id, updatedIssue)); } } }; diff --git a/web/components/issues/issue-layouts/list/base-list-root.tsx b/web/components/issues/issue-layouts/list/base-list-root.tsx index 8a3d87e40..5777f4e70 100644 --- a/web/components/issues/issue-layouts/list/base-list-root.tsx +++ b/web/components/issues/issue-layouts/list/base-list-root.tsx @@ -1,68 +1,46 @@ import { FC, useCallback } from "react"; import { observer } from "mobx-react-lite"; // types -import { TCreateModalStoreTypes } from "constants/issue"; +import { EIssuesStoreType } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; import { useIssues, useUser } from "hooks/store"; -import { IArchivedIssuesFilter, IArchivedIssues } from "store/issue/archived"; -import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle"; -import { IDraftIssuesFilter, IDraftIssues } from "store/issue/draft"; -import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module"; -import { IProfileIssues, IProfileIssuesFilter } from "store/issue/profile"; -import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project"; -import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views"; -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../types"; + +import { TIssue } from "@plane/types" // components import { List } from "./default"; import { IQuickActionProps } from "./list-view-types"; +import { useIssuesActions } from "hooks/use-issues-actions"; // constants // hooks +type ListStoreType = + | EIssuesStoreType.PROJECT + | EIssuesStoreType.MODULE + | EIssuesStoreType.CYCLE + | EIssuesStoreType.PROJECT_VIEW + | EIssuesStoreType.ARCHIVED + | EIssuesStoreType.DRAFT + | EIssuesStoreType.PROFILE; interface IBaseListRoot { - issuesFilter: - | IProjectIssuesFilter - | IModuleIssuesFilter - | ICycleIssuesFilter - | IProjectViewIssuesFilter - | IProfileIssuesFilter - | IDraftIssuesFilter - | IArchivedIssuesFilter; - issues: - | IProjectIssues - | ICycleIssues - | IModuleIssues - | IProjectViewIssues - | IProfileIssues - | IDraftIssues - | IArchivedIssues; QuickActions: FC; - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; - [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; - [EIssueActions.RESTORE]?: (issue: TIssue) => Promise; - }; viewId?: string; - storeType: TCreateModalStoreTypes; + storeType: ListStoreType; addIssuesToView?: (issueIds: string[]) => Promise; canEditPropertiesBasedOnProject?: (projectId: string) => boolean; isCompletedCycle?: boolean; } - export const BaseListRoot = observer((props: IBaseListRoot) => { const { - issuesFilter, - issues, QuickActions, - issueActions, viewId, storeType, addIssuesToView, canEditPropertiesBasedOnProject, isCompletedCycle = false, } = props; + + const { issuesFilter, issues } = useIssues(storeType); + const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue } = useIssuesActions(storeType); // mobx store const { membership: { currentProjectRole }, @@ -80,7 +58,7 @@ export const BaseListRoot = observer((props: IBaseListRoot) => { const isEditingAllowedBasedOnProject = canEditPropertiesBasedOnProject && projectId ? canEditPropertiesBasedOnProject(projectId) : isEditingAllowed; - return enableInlineEditing && isEditingAllowedBasedOnProject; + return !!enableInlineEditing && isEditingAllowedBasedOnProject; }, [canEditPropertiesBasedOnProject, enableInlineEditing, isEditingAllowed] ); @@ -91,37 +69,20 @@ export const BaseListRoot = observer((props: IBaseListRoot) => { const group_by = displayFilters?.group_by || null; const showEmptyGroup = displayFilters?.show_empty_groups ?? false; - const handleIssues = useCallback( - async (issue: TIssue, action: EIssueActions) => { - if (issueActions[action]) { - await issueActions[action]!(issue); - } - }, - [issueActions] - ); - const renderQuickActions = useCallback( (issue: TIssue) => ( handleIssues(issue, EIssueActions.DELETE)} - handleUpdate={ - issueActions[EIssueActions.UPDATE] ? async (data) => handleIssues(data, EIssueActions.UPDATE) : undefined - } - handleRemoveFromView={ - issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined - } - handleArchive={ - issueActions[EIssueActions.ARCHIVE] ? async () => handleIssues(issue, EIssueActions.ARCHIVE) : undefined - } - handleRestore={ - issueActions[EIssueActions.RESTORE] ? async () => handleIssues(issue, EIssueActions.RESTORE) : undefined - } + handleDelete={async () => removeIssue(issue.project_id, issue.id)} + handleUpdate={async (data) => updateIssue && updateIssue(issue.project_id, issue.id, data)} + handleRemoveFromView={async () => removeIssueFromView && removeIssueFromView(issue.project_id, issue.id)} + handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)} + handleRestore={async () => restoreIssue && restoreIssue(issue.project_id, issue.id)} readOnly={!isEditingAllowed || isCompletedCycle} /> ), // eslint-disable-next-line react-hooks/exhaustive-deps - [handleIssues] + [isEditingAllowed, isCompletedCycle, removeIssue, updateIssue, removeIssueFromView, archiveIssue, restoreIssue] ); return ( @@ -130,7 +91,7 @@ export const BaseListRoot = observer((props: IBaseListRoot) => { issuesMap={issueMap} displayProperties={displayProperties} group_by={group_by} - handleIssues={handleIssues} + updateIssue={updateIssue} quickActions={renderQuickActions} issueIds={issueIds} showEmptyGroup={showEmptyGroup} diff --git a/web/components/issues/issue-layouts/list/block.tsx b/web/components/issues/issue-layouts/list/block.tsx index a2148634c..099137348 100644 --- a/web/components/issues/issue-layouts/list/block.tsx +++ b/web/components/issues/issue-layouts/list/block.tsx @@ -9,19 +9,18 @@ import { useApplication, useIssueDetail, useProject } from "hooks/store"; // types import { TIssue, IIssueDisplayProperties, TIssueMap } from "@plane/types"; import { IssueProperties } from "../properties/all-properties"; -import { EIssueActions } from "../types"; interface IssueBlockProps { issueId: string; issuesMap: TIssueMap; - handleIssues: (issue: TIssue, action: EIssueActions) => Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue) => React.ReactNode; displayProperties: IIssueDisplayProperties | undefined; canEditProperties: (projectId: string | undefined) => boolean; } export const IssueBlock: React.FC = observer((props: IssueBlockProps) => { - const { issuesMap, issueId, handleIssues, quickActions, displayProperties, canEditProperties } = props; + const { issuesMap, issueId, updateIssue, quickActions, displayProperties, canEditProperties } = props; // hooks const { router: { workspaceSlug }, @@ -29,10 +28,6 @@ export const IssueBlock: React.FC = observer((props: IssueBlock const { getProjectIdentifierById } = useProject(); const { peekIssue, setPeekIssue } = useIssueDetail(); - const updateIssue = async (issueToUpdate: TIssue) => { - await handleIssues(issueToUpdate, EIssueActions.UPDATE); - }; - const handleIssuePeekOverview = (issue: TIssue) => workspaceSlug && issue && @@ -91,7 +86,7 @@ export const IssueBlock: React.FC = observer((props: IssueBlock className="relative flex items-center gap-2 whitespace-nowrap" issue={issue} isReadOnly={!canEditIssueProperties} - handleIssues={updateIssue} + updateIssue={updateIssue} displayProperties={displayProperties} activeLayout="List" /> diff --git a/web/components/issues/issue-layouts/list/blocks-list.tsx b/web/components/issues/issue-layouts/list/blocks-list.tsx index 23c364b67..2296e7b68 100644 --- a/web/components/issues/issue-layouts/list/blocks-list.tsx +++ b/web/components/issues/issue-layouts/list/blocks-list.tsx @@ -4,20 +4,19 @@ import RenderIfVisible from "components/core/render-if-visible-HOC"; import { IssueBlock } from "components/issues"; // types import { TGroupedIssues, TIssue, IIssueDisplayProperties, TIssueMap, TUnGroupedIssues } from "@plane/types"; -import { EIssueActions } from "../types"; interface Props { issueIds: TGroupedIssues | TUnGroupedIssues | any; issuesMap: TIssueMap; canEditProperties: (projectId: string | undefined) => boolean; - handleIssues: (issue: TIssue, action: EIssueActions) => Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue) => React.ReactNode; displayProperties: IIssueDisplayProperties | undefined; containerRef: MutableRefObject; } export const IssueBlocksList: FC = (props) => { - const { issueIds, issuesMap, handleIssues, quickActions, displayProperties, canEditProperties, containerRef } = props; + const { issueIds, issuesMap, updateIssue, quickActions, displayProperties, canEditProperties, containerRef } = props; return (
@@ -35,7 +34,7 @@ export const IssueBlocksList: FC = (props) => { Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue) => React.ReactNode; displayProperties: IIssueDisplayProperties | undefined; enableIssueQuickAdd: boolean; @@ -36,7 +35,7 @@ export interface IGroupByList { viewId?: string ) => Promise; disableIssueCreation?: boolean; - storeType: TCreateModalStoreTypes; + storeType: EIssuesStoreType; addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; isCompletedCycle?: boolean; @@ -47,7 +46,7 @@ const GroupByList: React.FC = (props) => { issueIds, issuesMap, group_by, - handleIssues, + updateIssue, quickActions, displayProperties, enableIssueQuickAdd, @@ -142,7 +141,7 @@ const GroupByList: React.FC = (props) => { Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue) => React.ReactNode; displayProperties: IIssueDisplayProperties | undefined; showEmptyGroup: boolean; @@ -184,7 +183,7 @@ export interface IList { ) => Promise; viewId?: string; disableIssueCreation?: boolean; - storeType: TCreateModalStoreTypes; + storeType: EIssuesStoreType; addIssuesToView?: (issueIds: string[]) => Promise; isCompletedCycle?: boolean; } @@ -194,7 +193,7 @@ export const List: React.FC = (props) => { issueIds, issuesMap, group_by, - handleIssues, + updateIssue, quickActions, quickAddCallback, viewId, @@ -214,7 +213,7 @@ export const List: React.FC = (props) => { issueIds={issueIds as TUnGroupedIssues} issuesMap={issuesMap} group_by={group_by} - handleIssues={handleIssues} + updateIssue={updateIssue} quickActions={quickActions} displayProperties={displayProperties} enableIssueQuickAdd={enableIssueQuickAdd} 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 acf26adb5..fa1a393c4 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 @@ -10,7 +10,7 @@ import { CreateUpdateIssueModal } from "components/issues"; // ui // mobx // hooks -import { TCreateModalStoreTypes } from "constants/issue"; +import { EIssuesStoreType } from "constants/issue"; import { useEventTracker } from "hooks/store"; // types import { TIssue, ISearchIssueResponse } from "@plane/types"; @@ -21,7 +21,7 @@ interface IHeaderGroupByCard { count: number; issuePayload: Partial; disableIssueCreation?: boolean; - storeType: TCreateModalStoreTypes; + storeType: EIssuesStoreType; addIssuesToView?: (issueIds: string[]) => Promise; } diff --git a/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx b/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx index 2f3807beb..73f8e3d3b 100644 --- a/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx @@ -1,47 +1,20 @@ -import { FC, useMemo } from "react"; +import { FC } from "react"; import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; // hooks import { ArchivedIssueQuickActions } from "components/issues"; import { EIssuesStoreType } from "constants/issue"; -import { useIssues } from "hooks/store"; // components // types -import { TIssue } from "@plane/types"; // constants -import { EIssueActions } from "../../types"; import { BaseListRoot } from "../base-list-root"; export const ArchivedIssueListLayout: FC = observer(() => { - const router = useRouter(); - const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; - - const { issues, issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED); - const issueActions = useMemo( - () => ({ - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.removeIssue(workspaceSlug, projectId, issue.id); - }, - [EIssueActions.RESTORE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.restoreIssue(workspaceSlug, projectId, issue.id); - }, - }), - [issues, workspaceSlug, projectId] - ); - const canEditPropertiesBasedOnProject = () => false; return ( ); diff --git a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx index 46ee7f32e..26afdf25b 100644 --- a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo } from "react"; +import React, { useCallback } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks @@ -7,9 +7,7 @@ import { EIssuesStoreType } from "constants/issue"; import { useCycle, useIssues } from "hooks/store"; // components // types -import { TIssue } from "@plane/types"; // constants -import { EIssueActions } from "../../types"; import { BaseListRoot } from "../base-list-root"; export interface ICycleListLayout {} @@ -18,34 +16,9 @@ export const CycleListLayout: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; // store - const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); + const { issues } = useIssues(EIssuesStoreType.CYCLE); const { currentProjectCompletedCycleIds } = useCycle(); - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, cycleId.toString()); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.removeIssueFromCycle(workspaceSlug.toString(), issue.project_id, cycleId.toString(), issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); - }, - }), - [issues, workspaceSlug, cycleId] - ); const isCompletedCycle = cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false; @@ -61,10 +34,7 @@ export const CycleListLayout: React.FC = observer(() => { return ( { const router = useRouter(); - const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; + const { workspaceSlug, projectId } = router.query; if (!workspaceSlug || !projectId) return null; - // store - const { issues, issuesFilter } = useIssues(EIssuesStoreType.DRAFT); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.updateIssue(workspaceSlug, projectId, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.removeIssue(workspaceSlug, projectId, issue.id); - }, - }), - [issues, workspaceSlug, projectId] - ); - - return ( - - ); + return ; }); diff --git a/web/components/issues/issue-layouts/list/roots/module-root.tsx b/web/components/issues/issue-layouts/list/roots/module-root.tsx index aca528a6a..3c6a8894a 100644 --- a/web/components/issues/issue-layouts/list/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/module-root.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from "react"; +import React from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // mobx store @@ -7,8 +7,6 @@ import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components // types -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../../types"; // constants import { BaseListRoot } from "../base-list-root"; @@ -18,40 +16,11 @@ export const ModuleListLayout: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId, moduleId } = router.query; - const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, moduleId.toString()); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, moduleId.toString()); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.removeIssueFromModule(workspaceSlug.toString(), issue.project_id, moduleId.toString(), issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id, moduleId.toString()); - }, - }), - [issues, workspaceSlug, moduleId] - ); + const { issues } = useIssues(EIssuesStoreType.MODULE); return ( { diff --git a/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx index dc0c68cd8..f24683d95 100644 --- a/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx @@ -1,50 +1,20 @@ -import { FC, useMemo } from "react"; +import { FC } from "react"; import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; // hooks import { ProjectIssueQuickActions } from "components/issues"; import { EIssuesStoreType } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; -import { useIssues, useUser } from "hooks/store"; +import { useUser } from "hooks/store"; // components // types -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../../types"; // constants import { BaseListRoot } from "../base-list-root"; export const ProfileIssuesListLayout: FC = observer(() => { - // router - const router = useRouter(); - const { workspaceSlug, userId } = router.query as { workspaceSlug: string; userId: string }; - // store hooks - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROFILE); - const { membership: { currentWorkspaceAllProjectsRole }, } = useUser(); - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !userId) return; - - await issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue, userId); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !userId) return; - - await issues.removeIssue(workspaceSlug, issue.project_id, issue.id, userId); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !userId) return; - - await issues.archiveIssue(workspaceSlug, issue.project_id, issue.id, userId); - }, - }), - [issues, workspaceSlug, userId] - ); - const canEditPropertiesBasedOnProject = (projectId: string) => { const currentProjectRole = currentWorkspaceAllProjectsRole && currentWorkspaceAllProjectsRole[projectId]; @@ -53,10 +23,7 @@ export const ProfileIssuesListLayout: FC = observer(() => { return ( diff --git a/web/components/issues/issue-layouts/list/roots/project-root.tsx b/web/components/issues/issue-layouts/list/roots/project-root.tsx index 8a0935979..fbbd26ffb 100644 --- a/web/components/issues/issue-layouts/list/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/project-root.tsx @@ -1,55 +1,19 @@ -import { FC, useMemo } from "react"; +import { FC } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks import { ProjectIssueQuickActions } from "components/issues"; import { EIssuesStoreType } from "constants/issue"; -import { useIssues } from "hooks/store"; // components // types -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../../types"; // constants import { BaseListRoot } from "../base-list-root"; export const ListLayout: FC = observer(() => { const router = useRouter(); - const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; + const { workspaceSlug, projectId } = router.query; if (!workspaceSlug || !projectId) return null; - // store - const { issuesFilter, issues } = useIssues(EIssuesStoreType.PROJECT); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.updateIssue(workspaceSlug, projectId, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.removeIssue(workspaceSlug, projectId, issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.archiveIssue(workspaceSlug, projectId, issue.id); - }, - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [issues] - ); - - return ( - - ); + return ; }); diff --git a/web/components/issues/issue-layouts/list/roots/project-view-root.tsx b/web/components/issues/issue-layouts/list/roots/project-view-root.tsx index 82ca03d42..260dd54bd 100644 --- a/web/components/issues/issue-layouts/list/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/project-view-root.tsx @@ -3,29 +3,13 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // store import { EIssuesStoreType } from "constants/issue"; -import { useIssues } from "hooks/store"; // constants // types -import { TIssue } from "@plane/types"; import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; -import { EIssueActions } from "../../types"; // components import { BaseListRoot } from "../base-list-root"; -export interface IViewListLayout { - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; - [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; - }; -} - -export const ProjectViewListLayout: React.FC = observer((props) => { - const { issueActions } = props; - // store - const { issuesFilter, issues } = useIssues(EIssuesStoreType.PROJECT_VIEW); - +export const ProjectViewListLayout: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId, viewId } = router.query; @@ -33,10 +17,7 @@ export const ProjectViewListLayout: React.FC = observer((props) return ( diff --git a/web/components/issues/issue-layouts/properties/all-properties.tsx b/web/components/issues/issue-layouts/properties/all-properties.tsx index c3a6bc037..8c1e33b8c 100644 --- a/web/components/issues/issue-layouts/properties/all-properties.tsx +++ b/web/components/issues/issue-layouts/properties/all-properties.tsx @@ -30,7 +30,7 @@ import { WithDisplayPropertiesHOC } from "../properties/with-display-properties- export interface IIssueProperties { issue: TIssue; - handleIssues: (issue: TIssue) => Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; displayProperties: IIssueDisplayProperties | undefined; isReadOnly: boolean; className: string; @@ -38,7 +38,7 @@ export interface IIssueProperties { } export const IssueProperties: React.FC = observer((props) => { - const { issue, handleIssues, displayProperties, activeLayout, isReadOnly, className } = props; + const { issue, updateIssue, displayProperties, activeLayout, isReadOnly, className } = props; // store hooks const { labelMap } = useLabel(); const { captureIssueEvent } = useEventTracker(); @@ -80,59 +80,63 @@ export const IssueProperties: React.FC = observer((props) => { ); const handleState = (stateId: string) => { - handleIssues({ ...issue, state_id: stateId }).then(() => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: router.asPath, - updates: { - changed_property: "state", - change_details: stateId, - }, + updateIssue && + updateIssue(issue.project_id, issue.id, { state_id: stateId }).then(() => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: router.asPath, + updates: { + changed_property: "state", + change_details: stateId, + }, + }); }); - }); }; const handlePriority = (value: TIssuePriorities) => { - handleIssues({ ...issue, priority: value }).then(() => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: router.asPath, - updates: { - changed_property: "priority", - change_details: value, - }, + updateIssue && + updateIssue(issue.project_id, issue.id, { priority: value }).then(() => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: router.asPath, + updates: { + changed_property: "priority", + change_details: value, + }, + }); }); - }); }; const handleLabel = (ids: string[]) => { - handleIssues({ ...issue, label_ids: ids }).then(() => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: router.asPath, - updates: { - changed_property: "labels", - change_details: ids, - }, + updateIssue && + updateIssue(issue.project_id, issue.id, { label_ids: ids }).then(() => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: router.asPath, + updates: { + changed_property: "labels", + change_details: ids, + }, + }); }); - }); }; const handleAssignee = (ids: string[]) => { - handleIssues({ ...issue, assignee_ids: ids }).then(() => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: router.asPath, - updates: { - changed_property: "assignees", - change_details: ids, - }, + updateIssue && + updateIssue(issue.project_id, issue.id, { assignee_ids: ids }).then(() => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: router.asPath, + updates: { + changed_property: "assignees", + change_details: ids, + }, + }); }); - }); }; const handleModule = useCallback( @@ -175,45 +179,52 @@ export const IssueProperties: React.FC = observer((props) => { ); const handleStartDate = (date: Date | null) => { - handleIssues({ ...issue, start_date: date ? renderFormattedPayloadDate(date) : null }).then(() => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: router.asPath, - updates: { - changed_property: "start_date", - change_details: date ? renderFormattedPayloadDate(date) : null, - }, - }); - }); + updateIssue && + updateIssue(issue.project_id, issue.id, { start_date: date ? renderFormattedPayloadDate(date) : null }).then( + () => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: router.asPath, + updates: { + changed_property: "start_date", + change_details: date ? renderFormattedPayloadDate(date) : null, + }, + }); + } + ); }; const handleTargetDate = (date: Date | null) => { - handleIssues({ ...issue, target_date: date ? renderFormattedPayloadDate(date) : null }).then(() => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: router.asPath, - updates: { - changed_property: "target_date", - change_details: date ? renderFormattedPayloadDate(date) : null, - }, - }); - }); + updateIssue && + updateIssue(issue.project_id, issue.id, { target_date: date ? renderFormattedPayloadDate(date) : null }).then( + () => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: router.asPath, + updates: { + changed_property: "target_date", + change_details: date ? renderFormattedPayloadDate(date) : null, + }, + }); + } + ); }; const handleEstimate = (value: number | null) => { - handleIssues({ ...issue, estimate_point: value }).then(() => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: router.asPath, - updates: { - changed_property: "estimate_point", - change_details: value, - }, + updateIssue && + updateIssue(issue.project_id, issue.id, { estimate_point: value }).then(() => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: router.asPath, + updates: { + changed_property: "estimate_point", + change_details: value, + }, + }); }); - }); }; const redirectToIssueDetail = () => { diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx index 5f7609a03..f6c63191f 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx @@ -90,7 +90,7 @@ export const AllIssueQuickActions: React.FC = observer((props }} data={issueToEdit ?? duplicateIssuePayload} onSubmit={async (data) => { - if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data }); + if (issueToEdit && handleUpdate) await handleUpdate(data); }} storeType={EIssuesStoreType.PROJECT} /> diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx index 89beda00c..fe713ed23 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx @@ -101,7 +101,7 @@ export const CycleIssueQuickActions: React.FC = observer((pro }} data={issueToEdit ?? duplicateIssuePayload} onSubmit={async (data) => { - if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data }); + if (issueToEdit && handleUpdate) await handleUpdate(data); }} storeType={EIssuesStoreType.CYCLE} /> diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx index 26eb6997c..f24f6869e 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx @@ -100,7 +100,7 @@ export const ModuleIssueQuickActions: React.FC = observer((pr }} data={issueToEdit ?? duplicateIssuePayload} onSubmit={async (data) => { - if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data }); + if (issueToEdit && handleUpdate) await handleUpdate(data); }} storeType={EIssuesStoreType.MODULE} /> diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx index 33b73f88c..24a2433d5 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx @@ -100,7 +100,7 @@ export const ProjectIssueQuickActions: React.FC = observer((p }} data={issueToEdit ?? duplicateIssuePayload} onSubmit={async (data) => { - if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data }); + if (issueToEdit && handleUpdate) await handleUpdate(data); }} storeType={EIssuesStoreType.PROJECT} isDraft={isDraftIssue} diff --git a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx index 1367eccc4..bcba7152e 100644 --- a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, useCallback, useMemo } from "react"; +import React, { Fragment, useCallback } from "react"; import isEmpty from "lodash/isEmpty"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; @@ -6,6 +6,7 @@ import useSWR from "swr"; // hooks import { useWorkspaceIssueProperties } from "hooks/use-workspace-issue-properties"; import { useApplication, useEventTracker, useGlobalView, useIssues, useProject, useUser } from "hooks/store"; +import { useIssuesActions } from "hooks/use-issues-actions"; // components import { GlobalViewsAppliedFiltersRoot, IssuePeekOverview } from "components/issues"; import { SpreadsheetView } from "components/issues/issue-layouts"; @@ -14,7 +15,6 @@ import { EmptyState } from "components/empty-state"; import { SpreadsheetLayoutLoader } from "components/ui"; // types import { TIssue, IIssueDisplayFilterOptions } from "@plane/types"; -import { EIssueActions } from "../types"; // constants import { EUserProjectRoles } from "constants/project"; import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; @@ -30,8 +30,9 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { const { commandPalette: commandPaletteStore } = useApplication(); const { issuesFilter: { filters, fetchFilters, updateFilters }, - issues: { loader, groupedIssueIds, fetchIssues, updateIssue, removeIssue, archiveIssue }, + issues: { loader, groupedIssueIds, fetchIssues }, } = useIssues(EIssuesStoreType.GLOBAL); + const { updateIssue, removeIssue, archiveIssue } = useIssuesActions(EIssuesStoreType.GLOBAL); const { dataViewId, issueIds } = groupedIssueIds; const { @@ -111,41 +112,6 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { const issueFilters = globalViewId ? filters?.[globalViewId.toString()] : undefined; - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - const projectId = issue.project_id; - if (!workspaceSlug || !projectId || !globalViewId) return; - - await updateIssue(workspaceSlug.toString(), projectId, issue.id, issue, globalViewId.toString()); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - const projectId = issue.project_id; - if (!workspaceSlug || !projectId || !globalViewId) return; - - await removeIssue(workspaceSlug.toString(), projectId, issue.id, globalViewId.toString()); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - const projectId = issue.project_id; - if (!workspaceSlug || !projectId || !globalViewId) return; - - await archiveIssue(workspaceSlug.toString(), projectId, issue.id, globalViewId.toString()); - }, - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [updateIssue, removeIssue, workspaceSlug] - ); - - const handleIssues = useCallback( - async (issue: TIssue, action: EIssueActions) => { - if (action === EIssueActions.UPDATE) await issueActions[action]!(issue); - if (action === EIssueActions.DELETE) await issueActions[action]!(issue); - if (action === EIssueActions.ARCHIVE) await issueActions[action]!(issue); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - const handleDisplayFiltersUpdate = useCallback( (updatedDisplayFilter: Partial) => { if (!workspaceSlug || !globalViewId) return; @@ -166,14 +132,14 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { handleIssues({ ...issue }, EIssueActions.UPDATE)} - handleDelete={async () => handleIssues(issue, EIssueActions.DELETE)} - handleArchive={async () => handleIssues(issue, EIssueActions.ARCHIVE)} + handleDelete={async () => removeIssue(issue.project_id, issue.id)} + handleUpdate={async (data) => updateIssue && updateIssue(issue.project_id, issue.id, data)} + handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)} portalElement={portalElement} readOnly={!canEditProperties(issue.project_id)} /> ), - [canEditProperties, handleIssues] + [canEditProperties, removeIssue, updateIssue, archiveIssue] ); if (loader === "init-loader" || !globalViewId || globalViewId !== dataViewId || !issueIds) { @@ -213,7 +179,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { handleDisplayFilterUpdate={handleDisplayFiltersUpdate} issueIds={issueIds} quickActions={renderQuickActions} - handleIssues={handleIssues} + updateIssue={updateIssue} canEditProperties={canEditProperties} viewId={globalViewId} /> diff --git a/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx b/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx index dbd6c5f96..d15e65865 100644 --- a/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, useMemo } from "react"; +import React, { Fragment } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import useSWR from "swr"; @@ -19,8 +19,6 @@ import { ActiveLoader } from "components/ui"; import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // types -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../types"; export const ProjectViewLayoutRoot: React.FC = observer(() => { // router @@ -45,22 +43,6 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => { { revalidateIfStale: false, revalidateOnFocus: false } ); - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, issue, viewId?.toString()); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.removeIssue(workspaceSlug.toString(), projectId.toString(), issue.id, viewId?.toString()); - }, - }), - [issues, workspaceSlug, projectId, viewId] - ); - const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; if (!workspaceSlug || !projectId || !viewId) return <>; @@ -81,15 +63,15 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => {
{activeLayout === "list" ? ( - + ) : activeLayout === "kanban" ? ( - + ) : activeLayout === "calendar" ? ( - + ) : activeLayout === "gantt_chart" ? ( - + ) : activeLayout === "spreadsheet" ? ( - + ) : null}
diff --git a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx index 5a522a527..fa89b77ed 100644 --- a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx @@ -2,56 +2,44 @@ import { FC, useCallback } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks -import { EIssueFilterType } from "constants/issue"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; -import { useUser } from "hooks/store"; +import { useIssues, useUser } from "hooks/store"; +import { useIssuesActions } from "hooks/use-issues-actions"; // views // types // constants -import { ICycleIssuesFilter, ICycleIssues } from "store/issue/cycle"; -import { IModuleIssuesFilter, IModuleIssues } from "store/issue/module"; -import { IProjectIssuesFilter, IProjectIssues } from "store/issue/project"; -import { IProjectViewIssuesFilter, IProjectViewIssues } from "store/issue/project-views"; import { TIssue, IIssueDisplayFilterOptions, TUnGroupedIssues } from "@plane/types"; import { IQuickActionProps } from "../list/list-view-types"; -import { EIssueActions } from "../types"; import { SpreadsheetView } from "./spreadsheet-view"; +export type SpreadsheetStoreType = + | EIssuesStoreType.PROJECT + | EIssuesStoreType.MODULE + | EIssuesStoreType.CYCLE + | EIssuesStoreType.PROJECT_VIEW; interface IBaseSpreadsheetRoot { - issueFiltersStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; - issueStore: IProjectIssues | ICycleIssues | IModuleIssues | IProjectViewIssues; viewId?: string; QuickActions: FC; - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => void; - [EIssueActions.UPDATE]?: (issue: TIssue) => void; - [EIssueActions.REMOVE]?: (issue: TIssue) => void; - [EIssueActions.ARCHIVE]?: (issue: TIssue) => void; - [EIssueActions.RESTORE]?: (issue: TIssue) => Promise; - }; + storeType: SpreadsheetStoreType; canEditPropertiesBasedOnProject?: (projectId: string) => boolean; isCompletedCycle?: boolean; } export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { - const { - issueFiltersStore, - issueStore, - viewId, - QuickActions, - issueActions, - canEditPropertiesBasedOnProject, - isCompletedCycle = false, - } = props; + const { viewId, QuickActions, storeType, canEditPropertiesBasedOnProject, isCompletedCycle = false } = props; // router const router = useRouter(); - const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; + const { projectId } = router.query; // store hooks const { membership: { currentProjectRole }, } = useUser(); + const { issues, issuesFilter } = useIssues(storeType); + const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue, updateFilters } = + useIssuesActions(storeType); // derived values - const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issueStore?.viewFlags || {}; + const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issues?.viewFlags || {}; // user role validation const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; @@ -65,32 +53,17 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { [canEditPropertiesBasedOnProject, enableInlineEditing, isEditingAllowed] ); - const issueIds = (issueStore.groupedIssueIds ?? []) as TUnGroupedIssues; - - const handleIssues = useCallback( - async (issue: TIssue, action: EIssueActions) => { - if (issueActions[action]) { - issueActions[action]!(issue); - } - }, - [issueActions] - ); + const issueIds = (issues.groupedIssueIds ?? []) as TUnGroupedIssues; const handleDisplayFiltersUpdate = useCallback( (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !projectId) return; + if ( !projectId) return; - issueFiltersStore.updateFilters( - workspaceSlug, - projectId, - EIssueFilterType.DISPLAY_FILTERS, - { - ...updatedDisplayFilter, - }, - viewId - ); + updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, { + ...updatedDisplayFilter, + }); }, - [issueFiltersStore, projectId, workspaceSlug, viewId] + [ projectId, updateFilters] ); const renderQuickActions = useCallback( @@ -98,37 +71,28 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { handleIssues(issue, EIssueActions.DELETE)} - handleUpdate={ - issueActions[EIssueActions.UPDATE] ? async (data) => handleIssues(data, EIssueActions.UPDATE) : undefined - } - handleRemoveFromView={ - issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined - } - handleArchive={ - issueActions[EIssueActions.ARCHIVE] ? async () => handleIssues(issue, EIssueActions.ARCHIVE) : undefined - } - handleRestore={ - issueActions[EIssueActions.RESTORE] ? async () => handleIssues(issue, EIssueActions.RESTORE) : undefined - } + handleDelete={async () => removeIssue(issue.project_id, issue.id)} + handleUpdate={async (data) => updateIssue && updateIssue(issue.project_id, issue.id, data)} + handleRemoveFromView={async () => removeIssueFromView && removeIssueFromView(issue.project_id, issue.id)} + handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)} + handleRestore={async () => restoreIssue && restoreIssue(issue.project_id, issue.id)} portalElement={portalElement} readOnly={!isEditingAllowed || isCompletedCycle} /> ), - // eslint-disable-next-line react-hooks/exhaustive-deps - [handleIssues] + [isEditingAllowed, isCompletedCycle, removeIssue, updateIssue, removeIssueFromView, archiveIssue, restoreIssue] ); return ( Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; isEstimateEnabled: boolean; }; export const IssueColumn = observer((props: Props) => { - const { displayProperties, issueDetail, disableUserActions, property, handleIssues, isEstimateEnabled } = props; + const { displayProperties, issueDetail, disableUserActions, property, updateIssue, isEstimateEnabled } = props; // router const router = useRouter(); const tableCellRef = useRef(null); @@ -44,7 +43,8 @@ export const IssueColumn = observer((props: Props) => { , updates: any) => - handleIssues({ ...issue, ...data }, EIssueActions.UPDATE).then(() => { + updateIssue && + updateIssue(issue.project_id, issue.id, data).then(() => { captureIssueEvent({ eventName: "Issue updated", payload: { diff --git a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx index 161aa07ae..8a8ce29f4 100644 --- a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx @@ -18,7 +18,6 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector"; import { IIssueDisplayProperties, TIssue } from "@plane/types"; // local components import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; -import { EIssueActions } from "../types"; import { IssueColumn } from "./issue-column"; interface Props { @@ -30,7 +29,7 @@ interface Props { portalElement?: HTMLDivElement | null ) => React.ReactNode; canEditProperties: (projectId: string | undefined) => boolean; - handleIssues: (issue: TIssue, action: EIssueActions) => Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; portalElement: React.MutableRefObject; nestingLevel: number; issueId: string; @@ -46,7 +45,7 @@ export const SpreadsheetIssueRow = observer((props: Props) => { isEstimateEnabled, nestingLevel, portalElement, - handleIssues, + updateIssue, quickActions, canEditProperties, isScrolled, @@ -76,7 +75,7 @@ export const SpreadsheetIssueRow = observer((props: Props) => { canEditProperties={canEditProperties} nestingLevel={nestingLevel} isEstimateEnabled={isEstimateEnabled} - handleIssues={handleIssues} + updateIssue={updateIssue} portalElement={portalElement} isScrolled={isScrolled} isExpanded={isExpanded} @@ -96,7 +95,7 @@ export const SpreadsheetIssueRow = observer((props: Props) => { canEditProperties={canEditProperties} nestingLevel={nestingLevel + 1} isEstimateEnabled={isEstimateEnabled} - handleIssues={handleIssues} + updateIssue={updateIssue} portalElement={portalElement} isScrolled={isScrolled} containerRef={containerRef} @@ -116,7 +115,7 @@ interface IssueRowDetailsProps { portalElement?: HTMLDivElement | null ) => React.ReactNode; canEditProperties: (projectId: string | undefined) => boolean; - handleIssues: (issue: TIssue, action: EIssueActions) => Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; portalElement: React.MutableRefObject; nestingLevel: number; issueId: string; @@ -132,7 +131,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => { isEstimateEnabled, nestingLevel, portalElement, - handleIssues, + updateIssue, quickActions, canEditProperties, isScrolled, @@ -261,7 +260,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => { issueDetail={issueDetail} disableUserActions={disableUserActions} property={property} - handleIssues={handleIssues} + updateIssue={updateIssue} isEstimateEnabled={isEstimateEnabled} /> ))} diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx index 7a8977b22..b8b4fd08a 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx @@ -1,59 +1,32 @@ -import React, { useCallback, useMemo } from "react"; +import React, { useCallback } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // mobx store import { EIssuesStoreType } from "constants/issue"; -import { useCycle, useIssues } from "hooks/store"; +import { useCycle } from "hooks/store"; // components -import { TIssue } from "@plane/types"; import { CycleIssueQuickActions } from "../../quick-action-dropdowns"; -import { EIssueActions } from "../../types"; import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; export const CycleSpreadsheetLayout: React.FC = observer(() => { const router = useRouter(); - const { workspaceSlug, cycleId } = router.query as { workspaceSlug: string; cycleId: string }; - - const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); + const { cycleId } = router.query; const { currentProjectCompletedCycleIds } = useCycle(); - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue, cycleId); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - issues.removeIssue(workspaceSlug, issue.project_id, issue.id, cycleId); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - issues.removeIssueFromCycle(workspaceSlug, issue.project_id, cycleId, issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - issues.archiveIssue(workspaceSlug, issue.project_id, issue.id, cycleId); - }, - }), - [issues, workspaceSlug, cycleId] - ); - const isCompletedCycle = cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false; const canEditIssueProperties = useCallback(() => !isCompletedCycle, [isCompletedCycle]); + if (!cycleId) return null; + return ( ); }); diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx index c52b40527..a95919cdc 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx @@ -1,51 +1,23 @@ -import React, { useMemo } from "react"; +import React from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // mobx store import { EIssuesStoreType } from "constants/issue"; -import { useIssues } from "hooks/store"; // components -import { TIssue } from "@plane/types"; import { ModuleIssueQuickActions } from "../../quick-action-dropdowns"; -import { EIssueActions } from "../../types"; import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; export const ModuleSpreadsheetLayout: React.FC = observer(() => { const router = useRouter(); - const { workspaceSlug, moduleId } = router.query as { workspaceSlug: string; moduleId: string }; + const { moduleId } = router.query; - const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, moduleId); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - issues.removeIssue(workspaceSlug, issue.project_id, issue.id, moduleId); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - issues.removeIssueFromModule(workspaceSlug, issue.project_id, moduleId, issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - issues.archiveIssue(workspaceSlug, issue.project_id, issue.id, moduleId); - }, - }), - [issues, workspaceSlug, moduleId] - ); + if (!moduleId) return null; return ( ); }); diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx index cc570fd81..dc9d354a6 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx @@ -1,48 +1,10 @@ -import React, { useMemo } from "react"; +import React from "react"; import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; // mobx store import { EIssuesStoreType } from "constants/issue"; -import { useIssues } from "hooks/store"; - -import { TIssue } from "@plane/types"; import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; -import { EIssueActions } from "../../types"; import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; -export const ProjectSpreadsheetLayout: React.FC = observer(() => { - const router = useRouter(); - const { workspaceSlug } = router.query as { workspaceSlug: string }; - - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.removeIssue(workspaceSlug, issue.project_id, issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.archiveIssue(workspaceSlug, issue.project_id, issue.id); - }, - }), - [issues, workspaceSlug] - ); - - return ( - - ); -}); +export const ProjectSpreadsheetLayout: React.FC = observer(() => ( + +)); diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx index dd134e070..754d87c2f 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx @@ -3,39 +3,22 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // mobx store import { EIssuesStoreType } from "constants/issue"; -import { useIssues } from "hooks/store"; // components -import { TIssue } from "@plane/types"; import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; -import { EIssueActions } from "../../types"; import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; // types // constants -export interface IViewSpreadsheetLayout { - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; - [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; - }; -} - -export const ProjectViewSpreadsheetLayout: React.FC = observer((props) => { - const { issueActions } = props; +export const ProjectViewSpreadsheetLayout: React.FC = observer(() => { // router const router = useRouter(); const { viewId } = router.query; - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW); - return ( ); }); diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx index 4bb2cbeab..896d5a4dd 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx @@ -3,7 +3,6 @@ import { observer } from "mobx-react-lite"; //types import { useTableKeyboardNavigation } from "hooks/use-table-keyboard-navigation"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssue } from "@plane/types"; -import { EIssueActions } from "../types"; //components import { SpreadsheetIssueRow } from "./issue-row"; import { SpreadsheetHeader } from "./spreadsheet-header"; @@ -19,7 +18,7 @@ type Props = { customActionButton?: React.ReactElement, portalElement?: HTMLDivElement | null ) => React.ReactNode; - handleIssues: (issue: TIssue, action: EIssueActions) => Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; canEditProperties: (projectId: string | undefined) => boolean; portalElement: React.MutableRefObject; containerRef: MutableRefObject; @@ -34,7 +33,7 @@ export const SpreadsheetTable = observer((props: Props) => { isEstimateEnabled, portalElement, quickActions, - handleIssues, + updateIssue, canEditProperties, containerRef, } = props; @@ -95,7 +94,7 @@ export const SpreadsheetTable = observer((props: Props) => { canEditProperties={canEditProperties} nestingLevel={0} isEstimateEnabled={isEstimateEnabled} - handleIssues={handleIssues} + updateIssue={updateIssue} portalElement={portalElement} containerRef={containerRef} isScrolled={isScrolled} diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx index f71634ab8..ed243d312 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx @@ -5,7 +5,6 @@ import { Spinner } from "@plane/ui"; import { SpreadsheetQuickAddIssueForm } from "components/issues"; import { useProject } from "hooks/store"; import { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; -import { EIssueActions } from "../types"; import { SpreadsheetTable } from "./spreadsheet-table"; // types //hooks @@ -20,7 +19,7 @@ type Props = { customActionButton?: React.ReactElement, portalElement?: HTMLDivElement | null ) => React.ReactNode; - handleIssues: (issue: TIssue, action: EIssueActions) => Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; openIssuesListModal?: (() => void) | null; quickAddCallback?: ( workspaceSlug: string, @@ -41,7 +40,7 @@ export const SpreadsheetView: React.FC = observer((props) => { handleDisplayFilterUpdate, issueIds, quickActions, - handleIssues, + updateIssue, quickAddCallback, viewId, canEditProperties, @@ -75,7 +74,7 @@ export const SpreadsheetView: React.FC = observer((props) => { isEstimateEnabled={isEstimateEnabled} portalElement={portalRef} quickActions={quickActions} - handleIssues={handleIssues} + updateIssue={updateIssue} canEditProperties={canEditProperties} containerRef={containerRef} /> diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx index 03a9ae5b0..67d3c904d 100644 --- a/web/components/issues/issue-modal/form.tsx +++ b/web/components/issues/issue-modal/form.tsx @@ -29,6 +29,7 @@ import { FileService } from "services/file.service"; // components // ui // helpers +import { getChangedIssuefields } from "helpers/issue.helper"; // types import type { TIssue, ISearchIssueResponse } from "@plane/types"; @@ -126,7 +127,7 @@ export const IssueFormRoot: FC = observer((props) => { } = useIssueDetail(); // form info const { - formState: { errors, isDirty, isSubmitting }, + formState: { errors, isDirty, isSubmitting, dirtyFields }, handleSubmit, reset, watch, @@ -166,7 +167,15 @@ export const IssueFormRoot: FC = observer((props) => { const issueName = watch("name"); const handleFormSubmit = async (formData: Partial, is_draft_issue = false) => { - await onSubmit(formData, is_draft_issue); + const submitData = !data?.id + ? formData + : { + ...getChangedIssuefields(formData, dirtyFields as { [key: string]: boolean | undefined }), + project_id: getValues("project_id"), + id: data.id, + description_html: formData.description_html ?? "

", + }; + await onSubmit(submitData, is_draft_issue); setGptAssistantModal(false); @@ -761,3 +770,4 @@ export const IssueFormRoot: FC = observer((props) => { ); }); + diff --git a/web/components/issues/issue-modal/modal.tsx b/web/components/issues/issue-modal/modal.tsx index fa6c4fbb3..b4cf05fc8 100644 --- a/web/components/issues/issue-modal/modal.tsx +++ b/web/components/issues/issue-modal/modal.tsx @@ -6,7 +6,7 @@ import { Dialog, Transition } from "@headlessui/react"; import { TOAST_TYPE, setToast } from "@plane/ui"; import { ISSUE_CREATED, ISSUE_UPDATED } from "constants/event-tracker"; -import { EIssuesStoreType, TCreateModalStoreTypes } from "constants/issue"; +import { EIssuesStoreType } from "constants/issue"; import { useApplication, useEventTracker, @@ -17,6 +17,7 @@ import { useIssueDetail, } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; +import { useIssuesActions } from "hooks/use-issues-actions"; // components import type { TIssue } from "@plane/types"; import { DraftIssueLayout } from "./draft-issue-layout"; @@ -31,7 +32,7 @@ export interface IssuesModalProps { onClose: () => void; onSubmit?: (res: TIssue) => Promise; withDraftIssueWrapper?: boolean; - storeType?: TCreateModalStoreTypes; + storeType?: EIssuesStoreType; isDraft?: boolean; } @@ -53,41 +54,15 @@ export const CreateUpdateIssueModal: React.FC = observer((prop // store hooks const { captureIssueEvent } = useEventTracker(); const { - router: { workspaceSlug, projectId, cycleId, moduleId, viewId: projectViewId }, + router: { workspaceSlug, projectId, cycleId, moduleId }, } = useApplication(); const { workspaceProjectIds } = useProject(); const { fetchCycleDetails } = useCycle(); const { fetchModuleDetails } = useModule(); - const { issues: projectIssues } = useIssues(EIssuesStoreType.PROJECT); const { issues: moduleIssues } = useIssues(EIssuesStoreType.MODULE); const { issues: cycleIssues } = useIssues(EIssuesStoreType.CYCLE); - const { issues: viewIssues } = useIssues(EIssuesStoreType.PROJECT_VIEW); - const { issues: profileIssues } = useIssues(EIssuesStoreType.PROFILE); const { issues: draftIssues } = useIssues(EIssuesStoreType.DRAFT); const { fetchIssue } = useIssueDetail(); - // store mapping based on current store - const issueStores = { - [EIssuesStoreType.PROJECT]: { - store: projectIssues, - viewId: undefined, - }, - [EIssuesStoreType.PROJECT_VIEW]: { - store: viewIssues, - viewId: projectViewId, - }, - [EIssuesStoreType.PROFILE]: { - store: profileIssues, - viewId: undefined, - }, - [EIssuesStoreType.CYCLE]: { - store: cycleIssues, - viewId: cycleId, - }, - [EIssuesStoreType.MODULE]: { - store: moduleIssues, - viewId: moduleId, - }, - }; // router const router = useRouter(); // local storage @@ -95,7 +70,7 @@ export const CreateUpdateIssueModal: React.FC = observer((prop Record> >("draftedIssue", {}); // current store details - const { store: currentIssueStore, viewId } = issueStores[storeType]; + const { createIssue, updateIssue } = useIssuesActions(storeType); const fetchIssueDetail = async (issueId: string | undefined) => { if (!workspaceSlug) return; @@ -176,11 +151,9 @@ export const CreateUpdateIssueModal: React.FC = observer((prop try { const response = is_draft_issue ? await draftIssues.createIssue(workspaceSlug, payload.project_id, payload) - : await currentIssueStore.createIssue(workspaceSlug, payload.project_id, payload, viewId); + : createIssue && (await createIssue(payload.project_id, payload)); if (!response) throw new Error(); - currentIssueStore.fetchIssues(workspaceSlug, payload.project_id, "mutation", viewId); - if (payload.cycle_id && payload.cycle_id !== "" && storeType !== EIssuesStoreType.CYCLE) await addIssueToCycle(response, payload.cycle_id); if (payload.module_ids && payload.module_ids.length > 0 && storeType !== EIssuesStoreType.MODULE) @@ -217,7 +190,7 @@ export const CreateUpdateIssueModal: React.FC = observer((prop try { isDraft ? await draftIssues.updateIssue(workspaceSlug, payload.project_id, data.id, payload) - : await currentIssueStore.updateIssue(workspaceSlug, payload.project_id, data.id, payload, viewId); + : updateIssue && (await updateIssue(payload.project_id, data.id, payload)); setToast({ type: TOAST_TYPE.SUCCESS, @@ -234,7 +207,7 @@ export const CreateUpdateIssueModal: React.FC = observer((prop setToast({ type: TOAST_TYPE.ERROR, title: "Error!", - message: "Issue could not be created. Please try again.", + message: "Issue could not be updated. Please try again.", }); captureIssueEvent({ eventName: ISSUE_UPDATED, @@ -244,13 +217,8 @@ export const CreateUpdateIssueModal: React.FC = observer((prop } }; - const handleFormSubmit = async (formData: Partial, is_draft_issue: boolean = false) => { - if (!workspaceSlug || !formData.project_id || !storeType) return; - - const payload: Partial = { - ...formData, - description_html: formData.description_html ?? "

", - }; + const handleFormSubmit = async (payload: Partial, is_draft_issue: boolean = false) => { + if (!workspaceSlug || !payload.project_id || !storeType) return; let response: TIssue | undefined = undefined; if (!data?.id) response = await handleCreateIssue(payload, is_draft_issue); diff --git a/web/components/issues/peek-overview/root.tsx b/web/components/issues/peek-overview/root.tsx index 3eae8d3e8..37cd8f375 100644 --- a/web/components/issues/peek-overview/root.tsx +++ b/web/components/issues/peek-overview/root.tsx @@ -234,10 +234,10 @@ export const IssuePeekOverview: FC = observer((props) => { message: () => "Cycle remove from issue failed", }, }); - const response = await removeFromCyclePromise; + await removeFromCyclePromise; captureIssueEvent({ eventName: ISSUE_UPDATED, - payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" }, + payload: { issueId, state: "SUCCESS", element: "Issue peek-overview" }, updates: { changed_property: "cycle_id", change_details: "", diff --git a/web/components/profile/overview/activity.tsx b/web/components/profile/overview/activity.tsx index 4a6cf98be..c8af1ccf8 100644 --- a/web/components/profile/overview/activity.tsx +++ b/web/components/profile/overview/activity.tsx @@ -46,24 +46,24 @@ export const ProfileActivity = observer(() => { {userProfileActivity.results.map((activity) => (
- {activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? ( + {activity.actor_detail?.avatar && activity.actor_detail?.avatar !== "" ? ( {activity.actor_detail.display_name} ) : (
- {activity.actor_detail.display_name?.charAt(0)} + {activity.actor_detail?.display_name?.charAt(0)}
)}

- {currentUser?.id === activity.actor_detail.id ? "You" : activity.actor_detail.display_name}{" "} + {currentUser?.id === activity.actor_detail?.id ? "You" : activity.actor_detail?.display_name}{" "} {activity.field ? ( diff --git a/web/components/profile/sidebar.tsx b/web/components/profile/sidebar.tsx index 3121f5b2e..adb9c63d7 100644 --- a/web/components/profile/sidebar.tsx +++ b/web/components/profile/sidebar.tsx @@ -96,21 +96,22 @@ export const ProfileSidebar = observer(() => { )} {userProjectsData.user_data.display_name}

- {userProjectsData.user_data.avatar && userProjectsData.user_data.avatar !== "" ? ( + {userProjectsData.user_data?.avatar && userProjectsData.user_data?.avatar !== "" ? ( {userProjectsData.user_data.display_name} ) : (
- {userProjectsData.user_data.first_name?.[0]} + {userProjectsData.user_data?.first_name?.[0]}
)}
@@ -118,9 +119,9 @@ export const ProfileSidebar = observer(() => {

- {userProjectsData.user_data.first_name} {userProjectsData.user_data.last_name} + {userProjectsData.user_data?.first_name} {userProjectsData.user_data?.last_name}

-
({userProjectsData.user_data.display_name})
+
({userProjectsData.user_data?.display_name})
{userDetails.map((detail) => ( diff --git a/web/components/project/member-list-item.tsx b/web/components/project/member-list-item.tsx index 43c2ce2a8..55b1b3c9a 100644 --- a/web/components/project/member-list-item.tsx +++ b/web/components/project/member-list-item.tsx @@ -44,7 +44,7 @@ export const ProjectMemberListItem: React.FC = observer((props) => { const handleRemove = async () => { if (!workspaceSlug || !projectId || !userDetails) return; - if (userDetails.member.id === currentUser?.id) { + if (userDetails.member?.id === currentUser?.id) { await leaveProject(workspaceSlug.toString(), projectId.toString()) .then(async () => { captureEvent(PROJECT_MEMBER_LEAVE, { @@ -62,7 +62,7 @@ export const ProjectMemberListItem: React.FC = observer((props) => { }) ); } else - await removeMemberFromProject(workspaceSlug.toString(), projectId.toString(), userDetails.member.id).catch( + await removeMemberFromProject(workspaceSlug.toString(), projectId.toString(), userDetails.member?.id).catch( (err) => setToast({ type: TOAST_TYPE.ERROR, @@ -84,12 +84,12 @@ export const ProjectMemberListItem: React.FC = observer((props) => { />
- {userDetails.member.avatar && userDetails.member.avatar !== "" ? ( - + {userDetails.member?.avatar && userDetails.member?.avatar !== "" ? ( + {userDetails.member.display_name @@ -97,23 +97,23 @@ export const ProjectMemberListItem: React.FC = observer((props) => { ) : ( - {(userDetails.member.display_name ?? userDetails.member.email ?? "?")[0]} + {(userDetails.member?.display_name ?? userDetails.member?.email ?? "?")[0]} )}
- + - {userDetails.member.first_name} {userDetails.member.last_name} + {userDetails.member?.first_name} {userDetails.member?.last_name}
-

{userDetails.member.display_name}

+

{userDetails.member?.display_name}

{isAdmin && ( <> -

{userDetails.member.email}

+

{userDetails.member?.email}

)}
@@ -126,12 +126,12 @@ export const ProjectMemberListItem: React.FC = observer((props) => {
{ROLE[userDetails.role]} - {userDetails.member.id !== currentUser?.id && ( + {userDetails.member?.id !== currentUser?.id && ( @@ -142,7 +142,7 @@ export const ProjectMemberListItem: React.FC = observer((props) => { onChange={(value: EUserProjectRoles) => { if (!workspaceSlug || !projectId) return; - updateMember(workspaceSlug.toString(), projectId.toString(), userDetails.member.id, { + updateMember(workspaceSlug.toString(), projectId.toString(), userDetails.member?.id, { role: value, }).catch((err) => { const error = err.error; @@ -156,7 +156,7 @@ export const ProjectMemberListItem: React.FC = observer((props) => { }); }} disabled={ - userDetails.member.id === currentUser?.id || !currentProjectRole || currentProjectRole < userDetails.role + userDetails.member?.id === currentUser?.id || !currentProjectRole || currentProjectRole < userDetails.role } placement="bottom-end" > @@ -170,8 +170,8 @@ export const ProjectMemberListItem: React.FC = observer((props) => { ); })} - {(isAdmin || userDetails.member.id === currentUser?.id) && ( - + {(isAdmin || userDetails.member?.id === currentUser?.id) && ( +
diff --git a/web/components/account/sign-up-forms/unique-code.tsx b/web/components/account/sign-up-forms/unique-code.tsx index 28581aed4..82f9685b1 100644 --- a/web/components/account/sign-up-forms/unique-code.tsx +++ b/web/components/account/sign-up-forms/unique-code.tsx @@ -202,8 +202,8 @@ export const SignUpUniqueCodeForm: React.FC = (props) => { {resendTimerCode > 0 ? `Request new code in ${resendTimerCode}s` : isRequestingNewCode - ? "Requesting new code" - : "Request new code"} + ? "Requesting new code" + : "Request new code"}
diff --git a/web/components/analytics/custom-analytics/sidebar/sidebar.tsx b/web/components/analytics/custom-analytics/sidebar/sidebar.tsx index 7a7c52377..a48ea3c03 100644 --- a/web/components/analytics/custom-analytics/sidebar/sidebar.tsx +++ b/web/components/analytics/custom-analytics/sidebar/sidebar.tsx @@ -160,8 +160,8 @@ export const CustomAnalyticsSidebar: React.FC = observer((props) => { (cycleId ? cycleDetails?.created_at : moduleId - ? moduleDetails?.created_at - : projectDetails?.created_at) ?? "" + ? moduleDetails?.created_at + : projectDetails?.created_at) ?? "" )}
)} diff --git a/web/components/api-token/modal/form.tsx b/web/components/api-token/modal/form.tsx index 9fc160815..2bea1da0c 100644 --- a/web/components/api-token/modal/form.tsx +++ b/web/components/api-token/modal/form.tsx @@ -170,8 +170,8 @@ export const CreateApiTokenForm: React.FC = (props) => { {value === "custom" ? "Custom date" : selectedOption - ? selectedOption.label - : "Set expiration date"} + ? selectedOption.label + : "Set expiration date"}
} value={value} @@ -207,8 +207,8 @@ export const CreateApiTokenForm: React.FC = (props) => { ? `Expires ${renderFormattedDate(customDate)}` : null : watch("expired_at") - ? `Expires ${getExpiryDate(watch("expired_at") ?? "")}` - : null} + ? `Expires ${getExpiryDate(watch("expired_at") ?? "")}` + : null} )}
diff --git a/web/components/core/modals/gpt-assistant-popover.tsx b/web/components/core/modals/gpt-assistant-popover.tsx index ecb4aec52..4bee1af33 100644 --- a/web/components/core/modals/gpt-assistant-popover.tsx +++ b/web/components/core/modals/gpt-assistant-popover.tsx @@ -172,8 +172,8 @@ export const GptAssistantPopover: React.FC = (props) => { const generateResponseButtonText = isSubmitting ? "Generating response..." : response === "" - ? "Generate response" - : "Generate again"; + ? "Generate response" + : "Generate again"; return ( diff --git a/web/components/cycles/cycles-board-card.tsx b/web/components/cycles/cycles-board-card.tsx index 2eecb1ae9..da97f2d9d 100644 --- a/web/components/cycles/cycles-board-card.tsx +++ b/web/components/cycles/cycles-board-card.tsx @@ -78,8 +78,8 @@ export const CyclesBoardCard: FC = observer((props) => { ? cycleTotalIssues === 0 ? "0 Issue" : cycleTotalIssues === cycleDetails.completed_issues - ? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}` - : `${cycleDetails.completed_issues}/${cycleTotalIssues} Issues` + ? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}` + : `${cycleDetails.completed_issues}/${cycleTotalIssues} Issues` : "0 Issue"; const handleCopyText = (e: MouseEvent) => { diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index 06db83e0d..adf986123 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -216,8 +216,8 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { ? "0 Issue" : `${cycleDetails.progress_snapshot.completed_issues}/${cycleDetails.progress_snapshot.total_issues}` : cycleDetails.total_issues === 0 - ? "0 Issue" - : `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`; + ? "0 Issue" + : `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`; const daysLeft = findHowManyDaysLeft(cycleDetails.end_date); diff --git a/web/components/dashboard/widgets/issues-by-state-group.tsx b/web/components/dashboard/widgets/issues-by-state-group.tsx index 6857f7ef3..1f093986c 100644 --- a/web/components/dashboard/widgets/issues-by-state-group.tsx +++ b/web/components/dashboard/widgets/issues-by-state-group.tsx @@ -79,14 +79,14 @@ export const IssuesByStateGroupWidget: React.FC = observer((props) startedCount > 0 ? "started" : unStartedCount > 0 - ? "unstarted" - : backlogCount > 0 - ? "backlog" - : completedCount > 0 - ? "completed" - : canceledCount > 0 - ? "cancelled" - : null; + ? "unstarted" + : backlogCount > 0 + ? "backlog" + : completedCount > 0 + ? "completed" + : canceledCount > 0 + ? "cancelled" + : null; setActiveStateGroup(stateGroup); setDefaultStateGroup(stateGroup); diff --git a/web/components/estimates/create-update-estimate-modal.tsx b/web/components/estimates/create-update-estimate-modal.tsx index 3be83e319..bc2dfb77d 100644 --- a/web/components/estimates/create-update-estimate-modal.tsx +++ b/web/components/estimates/create-update-estimate-modal.tsx @@ -312,8 +312,8 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { ? "Updating Estimate..." : "Update Estimate" : isSubmitting - ? "Creating Estimate..." - : "Create Estimate"} + ? "Creating Estimate..." + : "Create Estimate"}
diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index 468900110..5ef1ebf2c 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -205,9 +205,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { className="ml-1.5 flex-shrink-0" placement="bottom-start" > - {currentProjectCycleIds?.map((cycleId) => ( - - ))} + {currentProjectCycleIds?.map((cycleId) => )} } /> diff --git a/web/components/headers/module-issues.tsx b/web/components/headers/module-issues.tsx index 2a6268b52..10717ecc3 100644 --- a/web/components/headers/module-issues.tsx +++ b/web/components/headers/module-issues.tsx @@ -206,9 +206,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => { className="ml-1.5 flex-shrink-0" placement="bottom-start" > - {projectModuleIds?.map((moduleId) => ( - - ))} + {projectModuleIds?.map((moduleId) => )} } /> diff --git a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx index a36b8cc47..8d2b56d2a 100644 --- a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx +++ b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx @@ -31,13 +31,7 @@ interface IBaseCalendarRoot { } export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { - const { - QuickActions, - storeType, - addIssuesToView, - viewId, - isCompletedCycle = false, - } = props; + const { QuickActions, storeType, addIssuesToView, viewId, isCompletedCycle = false } = props; // router const router = useRouter(); diff --git a/web/components/issues/issue-layouts/calendar/calendar.tsx b/web/components/issues/issue-layouts/calendar/calendar.tsx index 823866d98..efd785d3e 100644 --- a/web/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/components/issues/issue-layouts/calendar/calendar.tsx @@ -5,7 +5,15 @@ import { observer } from "mobx-react-lite"; import { Spinner } from "@plane/ui"; import { CalendarHeader, CalendarWeekDays, CalendarWeekHeader } from "components/issues"; // types -import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TGroupedIssues, TIssue, TIssueKanbanFilters, TIssueMap } from "@plane/types"; +import { + IIssueDisplayFilterOptions, + IIssueDisplayProperties, + IIssueFilterOptions, + TGroupedIssues, + TIssue, + TIssueKanbanFilters, + TIssueMap, +} from "@plane/types"; import { ICalendarWeek } from "./types"; // constants import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; diff --git a/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx b/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx index d483ebe91..3050bba72 100644 --- a/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx +++ b/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx @@ -9,7 +9,13 @@ import { Popover, Transition } from "@headlessui/react"; import { Check, ChevronUp } from "lucide-react"; import { ToggleSwitch } from "@plane/ui"; // types -import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TCalendarLayouts, TIssueKanbanFilters } from "@plane/types"; +import { + IIssueDisplayFilterOptions, + IIssueDisplayProperties, + IIssueFilterOptions, + TCalendarLayouts, + TIssueKanbanFilters, +} from "@plane/types"; // constants import { CALENDAR_LAYOUTS } from "constants/calendar"; import { EIssueFilterType } from "constants/issue"; diff --git a/web/components/issues/issue-layouts/calendar/roots/module-root.tsx b/web/components/issues/issue-layouts/calendar/roots/module-root.tsx index f6080630f..b112b8c3c 100644 --- a/web/components/issues/issue-layouts/calendar/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/module-root.tsx @@ -12,9 +12,9 @@ import { useIssues } from "hooks/store"; export const ModuleCalendarLayout: React.FC = observer(() => { const router = useRouter(); - const { workspaceSlug, projectId, moduleId } = router.query ; + const { workspaceSlug, projectId, moduleId } = router.query; - const {issues} = useIssues(EIssuesStoreType.MODULE) + const { issues } = useIssues(EIssuesStoreType.MODULE); if (!moduleId) return null; diff --git a/web/components/issues/issue-layouts/gantt/project-root.tsx b/web/components/issues/issue-layouts/gantt/project-root.tsx index 90fcca145..d8a2cd1a1 100644 --- a/web/components/issues/issue-layouts/gantt/project-root.tsx +++ b/web/components/issues/issue-layouts/gantt/project-root.tsx @@ -5,4 +5,4 @@ import { EIssuesStoreType } from "constants/issue"; // components import { BaseGanttRoot } from "./base-gantt-root"; -export const GanttLayout: React.FC = observer(() =>( )); +export const GanttLayout: React.FC = observer(() => ); diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx index 0a492b5f7..e90823c5b 100644 --- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -198,13 +198,9 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas let kanbanFilters = issuesFilter?.issueFilters?.kanbanFilters?.[toggle] || []; if (kanbanFilters.includes(value)) kanbanFilters = kanbanFilters.filter((_value) => _value != value); else kanbanFilters.push(value); - updateFilters( - projectId.toString(), - EIssueFilterType.KANBAN_FILTERS, - { - [toggle]: kanbanFilters, - } - ); + updateFilters(projectId.toString(), EIssueFilterType.KANBAN_FILTERS, { + [toggle]: kanbanFilters, + }); } }; diff --git a/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx index 49103bcd1..c36fcc960 100644 --- a/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx @@ -11,8 +11,8 @@ import { BaseKanBanRoot } from "../base-kanban-root"; export const ProfileIssuesKanBanLayout: React.FC = observer(() => { const { - membership: { currentWorkspaceAllProjectsRole }, -} = useUser(); + membership: { currentWorkspaceAllProjectsRole }, + } = useUser(); const canEditPropertiesBasedOnProject = (projectId: string) => { const currentProjectRole = currentWorkspaceAllProjectsRole && currentWorkspaceAllProjectsRole[projectId]; diff --git a/web/components/issues/issue-layouts/list/base-list-root.tsx b/web/components/issues/issue-layouts/list/base-list-root.tsx index 5777f4e70..ae198f1ae 100644 --- a/web/components/issues/issue-layouts/list/base-list-root.tsx +++ b/web/components/issues/issue-layouts/list/base-list-root.tsx @@ -5,7 +5,7 @@ import { EIssuesStoreType } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; import { useIssues, useUser } from "hooks/store"; -import { TIssue } from "@plane/types" +import { TIssue } from "@plane/types"; // components import { List } from "./default"; import { IQuickActionProps } from "./list-view-types"; diff --git a/web/components/issues/issue-layouts/properties/labels.tsx b/web/components/issues/issue-layouts/properties/labels.tsx index a57c60d6f..090f0ce56 100644 --- a/web/components/issues/issue-layouts/properties/labels.tsx +++ b/web/components/issues/issue-layouts/properties/labels.tsx @@ -224,8 +224,8 @@ export const IssuePropertyLabels: React.FC = observer((pro disabled ? "cursor-not-allowed text-custom-text-200" : value.length <= maxRender - ? "cursor-pointer" - : "cursor-pointer hover:bg-custom-background-80" + ? "cursor-pointer" + : "cursor-pointer hover:bg-custom-background-80" } ${buttonClassName}`} onClick={handleOnClick} > diff --git a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx index fa89b77ed..653cc28f2 100644 --- a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx @@ -57,13 +57,13 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { const handleDisplayFiltersUpdate = useCallback( (updatedDisplayFilter: Partial) => { - if ( !projectId) return; + if (!projectId) return; updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, { ...updatedDisplayFilter, }); }, - [ projectId, updateFilters] + [projectId, updateFilters] ); const renderQuickActions = useCallback( diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx index 67d3c904d..dc1f42198 100644 --- a/web/components/issues/issue-modal/form.tsx +++ b/web/components/issues/issue-modal/form.tsx @@ -770,4 +770,3 @@ export const IssueFormRoot: FC = observer((props) => { ); }); - diff --git a/web/components/modules/module-card-item.tsx b/web/components/modules/module-card-item.tsx index 8023657da..4873e009c 100644 --- a/web/components/modules/module-card-item.tsx +++ b/web/components/modules/module-card-item.tsx @@ -159,8 +159,8 @@ export const ModuleCardItem: React.FC = observer((props) => { ? !moduleTotalIssues || moduleTotalIssues === 0 ? "0 Issue" : moduleTotalIssues === moduleDetails.completed_issues - ? `${moduleTotalIssues} Issue${moduleTotalIssues > 1 ? "s" : ""}` - : `${moduleDetails.completed_issues}/${moduleTotalIssues} Issues` + ? `${moduleTotalIssues} Issue${moduleTotalIssues > 1 ? "s" : ""}` + : `${moduleDetails.completed_issues}/${moduleTotalIssues} Issues` : "0 Issue"; return ( diff --git a/web/components/notifications/notification-card.tsx b/web/components/notifications/notification-card.tsx index 03f75ca63..0e4904a7e 100644 --- a/web/components/notifications/notification-card.tsx +++ b/web/components/notifications/notification-card.tsx @@ -176,12 +176,12 @@ export const NotificationCard: React.FC = (props) => { {notificationField === "comment" ? "commented" : notificationField === "archived_at" - ? notification.data.issue_activity.new_value === "restore" - ? "restored the issue" - : "archived the issue" - : notificationField === "None" - ? null - : replaceUnderscoreIfSnakeCase(notificationField)}{" "} + ? notification.data.issue_activity.new_value === "restore" + ? "restored the issue" + : "archived the issue" + : notificationField === "None" + ? null + : replaceUnderscoreIfSnakeCase(notificationField)}{" "} {!["comment", "archived_at", "None"].includes(notificationField) ? "to" : ""} {" "} diff --git a/web/components/profile/overview/workload.tsx b/web/components/profile/overview/workload.tsx index 54e03b047..c7e7a94a0 100644 --- a/web/components/profile/overview/workload.tsx +++ b/web/components/profile/overview/workload.tsx @@ -25,8 +25,8 @@ export const ProfileWorkload: React.FC = ({ stateDistribution }) => ( {group.state_group === "unstarted" ? "Not started" : group.state_group === "started" - ? "Working on" - : STATE_GROUPS[group.state_group].label} + ? "Working on" + : STATE_GROUPS[group.state_group].label}

{group.state_count}

diff --git a/web/components/profile/sidebar.tsx b/web/components/profile/sidebar.tsx index adb9c63d7..4cab1a9f1 100644 --- a/web/components/profile/sidebar.tsx +++ b/web/components/profile/sidebar.tsx @@ -164,8 +164,8 @@ export const ProfileSidebar = observer(() => { completedIssuePercentage <= 35 ? "bg-red-500/10 text-red-500" : completedIssuePercentage <= 70 - ? "bg-yellow-500/10 text-yellow-500" - : "bg-green-500/10 text-green-500" + ? "bg-yellow-500/10 text-yellow-500" + : "bg-green-500/10 text-green-500" }`} > {completedIssuePercentage}% diff --git a/web/components/project/send-project-invitation-modal.tsx b/web/components/project/send-project-invitation-modal.tsx index cce96b9b7..24fc36521 100644 --- a/web/components/project/send-project-invitation-modal.tsx +++ b/web/components/project/send-project-invitation-modal.tsx @@ -142,9 +142,8 @@ export const SendProjectInvitationModal: React.FC = observer((props) => { if (!memberDetails?.member) return; return { value: `${memberDetails?.member.id}`, - query: `${memberDetails?.member.first_name} ${ - memberDetails?.member.last_name - } ${memberDetails?.member.display_name.toLowerCase()}`, + query: `${memberDetails?.member.first_name} ${memberDetails?.member + .last_name} ${memberDetails?.member.display_name.toLowerCase()}`, content: (
diff --git a/web/components/ui/multi-level-dropdown.tsx b/web/components/ui/multi-level-dropdown.tsx index 8633d1586..d66702ccc 100644 --- a/web/components/ui/multi-level-dropdown.tsx +++ b/web/components/ui/multi-level-dropdown.tsx @@ -108,12 +108,12 @@ export const MultiLevelDropdown: React.FC = ({ height === "sm" ? "max-h-28" : height === "md" - ? "max-h-44" - : height === "rg" - ? "max-h-56" - : height === "lg" - ? "max-h-80" - : "" + ? "max-h-44" + : height === "rg" + ? "max-h-56" + : height === "lg" + ? "max-h-80" + : "" }`} > {option.children ? ( diff --git a/web/helpers/dashboard.helper.ts b/web/helpers/dashboard.helper.ts index c8c2e7746..0c3b8da07 100644 --- a/web/helpers/dashboard.helper.ts +++ b/web/helpers/dashboard.helper.ts @@ -49,10 +49,10 @@ export const getRedirectionFilters = (type: TIssuesListTypes): string => { type === "pending" ? "?state_group=backlog,unstarted,started" : type === "upcoming" - ? `?target_date=${today};after` - : type === "overdue" - ? `?target_date=${today};before` - : "?state_group=completed"; + ? `?target_date=${today};after` + : type === "overdue" + ? `?target_date=${today};before` + : "?state_group=completed"; return filterParams; }; diff --git a/web/helpers/emoji.helper.tsx b/web/helpers/emoji.helper.tsx index 1fb746f51..513f9b6c4 100644 --- a/web/helpers/emoji.helper.tsx +++ b/web/helpers/emoji.helper.tsx @@ -41,13 +41,16 @@ export const groupReactions: (reactions: any[], key: string) => { [key: string]: reactions: any, key: string ) => { - const groupedReactions = reactions.reduce((acc: any, reaction: any) => { - if (!acc[reaction[key]]) { - acc[reaction[key]] = []; - } - acc[reaction[key]].push(reaction); - return acc; - }, {} as { [key: string]: any[] }); + const groupedReactions = reactions.reduce( + (acc: any, reaction: any) => { + if (!acc[reaction[key]]) { + acc[reaction[key]] = []; + } + acc[reaction[key]].push(reaction); + return acc; + }, + {} as { [key: string]: any[] } + ); return groupedReactions; }; diff --git a/web/helpers/issue.helper.ts b/web/helpers/issue.helper.ts index 0070ed201..3e6689151 100644 --- a/web/helpers/issue.helper.ts +++ b/web/helpers/issue.helper.ts @@ -172,19 +172,15 @@ export const renderIssueBlocksStructure = (blocks: TIssue[]): IGanttBlock[] => target_date: block.target_date ? new Date(block.target_date) : null, })); +export function getChangedIssuefields(formData: Partial, dirtyFields: { [key: string]: boolean | undefined }) { + const changedFields: Partial = {}; - export function getChangedIssuefields( - formData: Partial, - dirtyFields: { [key: string]: boolean | undefined } - ) { - const changedFields: Partial = {}; - - const dirtyFieldKeys = Object.keys(dirtyFields) as (keyof TIssue)[]; - for (const dirtyField of dirtyFieldKeys) { - if (!!dirtyFields[dirtyField]) { - changedFields[dirtyField] = formData[dirtyField]; - } + const dirtyFieldKeys = Object.keys(dirtyFields) as (keyof TIssue)[]; + for (const dirtyField of dirtyFieldKeys) { + if (!!dirtyFields[dirtyField]) { + changedFields[dirtyField] = formData[dirtyField]; } + } - return changedFields; - } \ No newline at end of file + return changedFields; +} diff --git a/web/layouts/settings-layout/profile/layout.tsx b/web/layouts/settings-layout/profile/layout.tsx index ed594c9f2..67f545a2d 100644 --- a/web/layouts/settings-layout/profile/layout.tsx +++ b/web/layouts/settings-layout/profile/layout.tsx @@ -21,9 +21,7 @@ export const ProfileSettingsLayout: FC = (props) => {
{header} -
- {children} -
+
{children}
diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx index 6e74de061..0b1b238a9 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx @@ -69,9 +69,10 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => { title: "Success", message: issue && - `${getProjectById(issue.project_id)?.identifier}-${ - issue?.sequence_id - } is restored successfully under the project ${getProjectById(issue.project_id)?.name}`, + `${getProjectById(issue.project_id) + ?.identifier}-${issue?.sequence_id} is restored successfully under the project ${getProjectById( + issue.project_id + )?.name}`, }); router.push(`/${workspaceSlug}/projects/${projectId}/issues/${archivedIssueId}`); }) diff --git a/web/store/member/workspace-member.store.ts b/web/store/member/workspace-member.store.ts index 66466a410..b1472a9d2 100644 --- a/web/store/member/workspace-member.store.ts +++ b/web/store/member/workspace-member.store.ts @@ -127,9 +127,8 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore { const searchedWorkspaceMemberIds = workspaceMemberIds?.filter((userId) => { const memberDetails = this.getWorkspaceMemberDetails(userId); if (!memberDetails) return false; - const memberSearchQuery = `${memberDetails.member.first_name} ${memberDetails.member.last_name} ${ - memberDetails.member?.display_name - } ${memberDetails.member.email ?? ""}`; + const memberSearchQuery = `${memberDetails.member.first_name} ${memberDetails.member.last_name} ${memberDetails + .member?.display_name} ${memberDetails.member.email ?? ""}`; return memberSearchQuery.toLowerCase()?.includes(searchQuery.toLowerCase()); }); return searchedWorkspaceMemberIds; From b535d8a23c8ccd3b75996754574be6d8ac777ad8 Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Thu, 7 Mar 2024 13:25:23 +0530 Subject: [PATCH 062/308] [WED-634] fix: profile issues not rendering correctly with GroupBy display filter (#3886) * fix: Handled issue render in the display filters groupBy stateGroup and labels * chore: Optimized state_group filter and typo * Fix: removed workspaceLevel boolean and handled the states from stateMap --- .../issues/issue-layouts/list/default.tsx | 5 +++++ web/components/issues/issue-layouts/utils.tsx | 16 +++++++++------- web/store/issue/helpers/issue-helper.store.ts | 12 ++++++------ web/store/issue/root.store.ts | 4 ++++ web/store/state.store.ts | 18 ++++++++++++++---- 5 files changed, 38 insertions(+), 17 deletions(-) diff --git a/web/components/issues/issue-layouts/list/default.tsx b/web/components/issues/issue-layouts/list/default.tsx index b536da0d3..12179ee97 100644 --- a/web/components/issues/issue-layouts/list/default.tsx +++ b/web/components/issues/issue-layouts/list/default.tsx @@ -77,6 +77,7 @@ const GroupByList: React.FC = (props) => { label, projectState, member, + true, true ); @@ -97,6 +98,10 @@ const GroupByList: React.FC = (props) => { preloadedData = { ...preloadedData, label_ids: [value] }; } else if (groupByKey === "assignees" && value != "None") { preloadedData = { ...preloadedData, assignee_ids: [value] }; + } else if (groupByKey === "cycle" && value != "None") { + preloadedData = { ...preloadedData, cycle_id: value }; + } else if (groupByKey === "module" && value != "None") { + preloadedData = { ...preloadedData, module_ids: [value] }; } else if (groupByKey === "created_by") { preloadedData = { ...preloadedData }; } else { diff --git a/web/components/issues/issue-layouts/utils.tsx b/web/components/issues/issue-layouts/utils.tsx index 3a459ba7a..ffe979a56 100644 --- a/web/components/issues/issue-layouts/utils.tsx +++ b/web/components/issues/issue-layouts/utils.tsx @@ -24,7 +24,8 @@ export const getGroupByColumns = ( label: ILabelStore, projectState: IStateStore, member: IMemberRootStore, - includeNone?: boolean + includeNone?: boolean, + isWorkspaceLevel?: boolean ): IGroupByColumn[] | undefined => { switch (groupBy) { case "project": @@ -40,7 +41,7 @@ export const getGroupByColumns = ( case "priority": return getPriorityColumns(); case "labels": - return getLabelsColumns(label) as any; + return getLabelsColumns(label, isWorkspaceLevel) as any; case "assignees": return getAssigneeColumns(member) as any; case "created_by": @@ -177,12 +178,13 @@ const getPriorityColumns = () => { })); }; -const getLabelsColumns = (label: ILabelStore) => { - const { projectLabels } = label; +const getLabelsColumns = (label: ILabelStore, isWorkspaceLevel: boolean = false) => { + const { workspaceLabels, projectLabels } = label; - if (!projectLabels) return; - - const labels = [...projectLabels, { id: "None", name: "None", color: "#666" }]; + const labels = [ + ...(isWorkspaceLevel ? workspaceLabels || [] : projectLabels || []), + { id: "None", name: "None", color: "#666" }, + ]; return labels.map((label) => ({ id: label.id, diff --git a/web/store/issue/helpers/issue-helper.store.ts b/web/store/issue/helpers/issue-helper.store.ts index 235f65e7c..50e04e890 100644 --- a/web/store/issue/helpers/issue-helper.store.ts +++ b/web/store/issue/helpers/issue-helper.store.ts @@ -76,8 +76,8 @@ export class IssueHelperStore implements TIssueHelperStore { let groupArray = []; if (groupBy === "state_detail.group") { - const state_group = - this.rootStore?.stateDetails?.find((_state) => _state.id === _issue?.state_id)?.group || "None"; + // if groupBy state_detail.group is coming from the project level the we are using stateDetails from root store else we are looping through the stateMap + const state_group = (this.rootStore?.stateMap || {})?.[_issue?.state_id]?.group || "None"; groupArray = [state_group]; } else { const groupValue = get(_issue, ISSUE_FILTER_DEFAULT_DATA[groupBy]); @@ -117,8 +117,8 @@ export class IssueHelperStore implements TIssueHelperStore { let subGroupArray = []; let groupArray = []; if (subGroupBy === "state_detail.group" || groupBy === "state_detail.group") { - const state_group = - this.rootStore?.stateDetails?.find((_state) => _state.id === _issue?.state_id)?.group || "None"; + const state_group = (this.rootStore?.stateMap || {})?.[_issue?.state_id]?.group || "None"; + subGroupArray = [state_group]; groupArray = [state_group]; } else { @@ -233,10 +233,10 @@ export class IssueHelperStore implements TIssueHelperStore { } /** - * This Method is mainly used to filter out empty values in the begining + * This Method is mainly used to filter out empty values in the beginning * @param key key of the value that is to be checked if empty * @param object any object in which the key's value is to be checked - * @returns 1 if emoty, 0 if not empty + * @returns 1 if empty, 0 if not empty */ getSortOrderToFilterEmptyValues(key: string, object: any) { const value = object?.[key]; diff --git a/web/store/issue/root.store.ts b/web/store/issue/root.store.ts index a9dde82ae..68206a704 100644 --- a/web/store/issue/root.store.ts +++ b/web/store/issue/root.store.ts @@ -35,6 +35,7 @@ export interface IIssueRootStore { userId: string | undefined; // user profile detail Id stateMap: Record | undefined; stateDetails: IState[] | undefined; + workspaceStateDetails: IState[] | undefined; labelMap: Record | undefined; workSpaceMemberRolesMap: Record | undefined; memberMap: Record | undefined; @@ -89,6 +90,7 @@ export class IssueRootStore implements IIssueRootStore { userId: string | undefined = undefined; stateMap: Record | undefined = undefined; stateDetails: IState[] | undefined = undefined; + workspaceStateDetails: IState[] | undefined = undefined; labelMap: Record | undefined = undefined; workSpaceMemberRolesMap: Record | undefined = undefined; memberMap: Record | undefined = undefined; @@ -142,6 +144,7 @@ export class IssueRootStore implements IIssueRootStore { globalViewId: observable.ref, stateMap: observable, stateDetails: observable, + workspaceStateDetails: observable, labelMap: observable, memberMap: observable, workSpaceMemberRolesMap: observable, @@ -163,6 +166,7 @@ export class IssueRootStore implements IIssueRootStore { if (rootStore.app.router.userId) this.userId = rootStore.app.router.userId; if (!isEmpty(rootStore?.state?.stateMap)) this.stateMap = rootStore?.state?.stateMap; if (!isEmpty(rootStore?.state?.projectStates)) this.stateDetails = rootStore?.state?.projectStates; + if (!isEmpty(rootStore?.state?.workspaceStates)) this.workspaceStateDetails = rootStore?.state?.workspaceStates; if (!isEmpty(rootStore?.label?.labelMap)) this.labelMap = rootStore?.label?.labelMap; if (!isEmpty(rootStore?.memberRoot?.workspace?.workspaceMemberMap)) this.workSpaceMemberRolesMap = rootStore?.memberRoot?.workspace?.memberMap || undefined; diff --git a/web/store/state.store.ts b/web/store/state.store.ts index df3496f39..eaece6db0 100644 --- a/web/store/state.store.ts +++ b/web/store/state.store.ts @@ -17,6 +17,7 @@ export interface IStateStore { // observables stateMap: Record; // computed + workspaceStates: IState[] | undefined; projectStates: IState[] | undefined; groupedProjectStates: Record | undefined; // computed actions @@ -73,13 +74,22 @@ export class StateStore implements IStateStore { this.router = _rootStore.app.router; } + /** + * Returns the stateMap belongs to a specific workspace + */ + get workspaceStates() { + const workspaceSlug = this.router.workspaceSlug || ""; + if (!workspaceSlug || !this.fetchedMap[workspaceSlug]) return; + return sortStates(Object.values(this.stateMap)); + } + /** * Returns the stateMap belongs to a specific project */ get projectStates() { const projectId = this.router.projectId; - const worksapceSlug = this.router.workspaceSlug || ""; - if (!projectId || !(this.fetchedMap[projectId] || this.fetchedMap[worksapceSlug])) return; + const workspaceSlug = this.router.workspaceSlug || ""; + if (!projectId || !(this.fetchedMap[projectId] || this.fetchedMap[workspaceSlug])) return; return sortStates(Object.values(this.stateMap).filter((state) => state.project_id === projectId)); } @@ -106,8 +116,8 @@ export class StateStore implements IStateStore { * @returns IState[] */ getProjectStates = computedFn((projectId: string) => { - const worksapceSlug = this.router.workspaceSlug || ""; - if (!projectId || !(this.fetchedMap[projectId] || this.fetchedMap[worksapceSlug])) return; + const workspaceSlug = this.router.workspaceSlug || ""; + if (!projectId || !(this.fetchedMap[projectId] || this.fetchedMap[workspaceSlug])) return; return sortStates(Object.values(this.stateMap).filter((state) => state.project_id === projectId)); }); From b03f6a81e20810f9f658f0052f38fd394510a9e0 Mon Sep 17 00:00:00 2001 From: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com> Date: Thu, 7 Mar 2024 13:26:58 +0530 Subject: [PATCH 063/308] fix: project members settings flickering (#3894) --- web/components/project/member-select.tsx | 4 ++-- .../project/project-settings-member-defaults.tsx | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/web/components/project/member-select.tsx b/web/components/project/member-select.tsx index 2aa83cd06..e6e0335e6 100644 --- a/web/components/project/member-select.tsx +++ b/web/components/project/member-select.tsx @@ -49,14 +49,14 @@ export const MemberSelect: React.FC = observer((props) => { +
{selectedOption && } {selectedOption ? ( selectedOption.member?.display_name ) : (
- None + None
)}
diff --git a/web/components/project/project-settings-member-defaults.tsx b/web/components/project/project-settings-member-defaults.tsx index d6cffa7a4..89695e891 100644 --- a/web/components/project/project-settings-member-defaults.tsx +++ b/web/components/project/project-settings-member-defaults.tsx @@ -63,11 +63,14 @@ export const ProjectSettingsMemberDefaults: React.FC = observer(() => { }); await updateProject(workspaceSlug.toString(), projectId.toString(), { - default_assignee: formData.default_assignee === "none" ? null : formData.default_assignee, - project_lead: formData.project_lead === "none" ? null : formData.project_lead, + default_assignee: + formData.default_assignee === "none" + ? null + : formData.default_assignee ?? currentProjectDetails?.default_assignee, + project_lead: + formData.project_lead === "none" ? null : formData.project_lead ?? currentProjectDetails?.project_lead, }) .then(() => { - fetchProjectDetails(workspaceSlug.toString(), projectId.toString()); setToast({ title: "Success", type: TOAST_TYPE.SUCCESS, From bc02e56e3ce171fff9f97a8faa88ac877f79c668 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Thu, 7 Mar 2024 13:33:12 +0530 Subject: [PATCH 064/308] [WEB-664] refactor: folder structure (#3884) * refactor: folder structure * chore: resolved merge conflicts --- apiserver/plane/app/urls/issue.py | 12 +- apiserver/plane/app/views/__init__.py | 171 +- .../views/{analytic.py => analytic/base.py} | 0 .../app/views/{asset.py => asset/base.py} | 2 +- .../app/views/{cycle.py => cycle/base.py} | 274 +- apiserver/plane/app/views/cycle/issue.py | 312 +++ .../views/{dashboard.py => dashboard/base.py} | 2 +- .../views/{estimate.py => estimate/base.py} | 2 +- .../views/{exporter.py => exporter/base.py} | 2 +- .../views/{external.py => external/base.py} | 2 +- .../app/views/{inbox.py => inbox/base.py} | 2 +- apiserver/plane/app/views/issue.py | 2462 ----------------- apiserver/plane/app/views/issue/activity.py | 85 + apiserver/plane/app/views/issue/archive.py | 347 +++ apiserver/plane/app/views/issue/attachment.py | 73 + apiserver/plane/app/views/issue/base.py | 686 +++++ apiserver/plane/app/views/issue/comment.py | 219 ++ apiserver/plane/app/views/issue/draft.py | 367 +++ apiserver/plane/app/views/issue/label.py | 105 + apiserver/plane/app/views/issue/link.py | 120 + apiserver/plane/app/views/issue/reaction.py | 89 + apiserver/plane/app/views/issue/relation.py | 204 ++ apiserver/plane/app/views/issue/sub_issue.py | 195 ++ apiserver/plane/app/views/issue/subscriber.py | 124 + .../app/views/{module.py => module/base.py} | 237 +- apiserver/plane/app/views/module/issue.py | 259 ++ .../{notification.py => notification/base.py} | 2 +- .../plane/app/views/{page.py => page/base.py} | 2 +- apiserver/plane/app/views/project.py | 1146 -------- apiserver/plane/app/views/project/base.py | 549 ++++ apiserver/plane/app/views/project/invite.py | 286 ++ apiserver/plane/app/views/project/member.py | 349 +++ .../app/views/{state.py => state/base.py} | 2 +- .../plane/app/views/{user.py => user/base.py} | 0 .../plane/app/views/{view.py => view/base.py} | 2 +- .../app/views/{webhook.py => webhook/base.py} | 2 +- apiserver/plane/app/views/workspace.py | 1843 ------------ apiserver/plane/app/views/workspace/base.py | 414 +++ apiserver/plane/app/views/workspace/cycle.py | 116 + .../plane/app/views/workspace/estimate.py | 39 + apiserver/plane/app/views/workspace/invite.py | 301 ++ apiserver/plane/app/views/workspace/label.py | 25 + apiserver/plane/app/views/workspace/member.py | 396 +++ apiserver/plane/app/views/workspace/module.py | 104 + apiserver/plane/app/views/workspace/state.py | 25 + apiserver/plane/app/views/workspace/user.py | 573 ++++ 46 files changed, 6495 insertions(+), 6034 deletions(-) rename apiserver/plane/app/views/{analytic.py => analytic/base.py} (100%) rename apiserver/plane/app/views/{asset.py => asset/base.py} (98%) rename apiserver/plane/app/views/{cycle.py => cycle/base.py} (78%) create mode 100644 apiserver/plane/app/views/cycle/issue.py rename apiserver/plane/app/views/{dashboard.py => dashboard/base.py} (99%) rename apiserver/plane/app/views/{estimate.py => estimate/base.py} (99%) rename apiserver/plane/app/views/{exporter.py => exporter/base.py} (99%) rename apiserver/plane/app/views/{external.py => external/base.py} (99%) rename apiserver/plane/app/views/{inbox.py => inbox/base.py} (99%) delete mode 100644 apiserver/plane/app/views/issue.py create mode 100644 apiserver/plane/app/views/issue/activity.py create mode 100644 apiserver/plane/app/views/issue/archive.py create mode 100644 apiserver/plane/app/views/issue/attachment.py create mode 100644 apiserver/plane/app/views/issue/base.py create mode 100644 apiserver/plane/app/views/issue/comment.py create mode 100644 apiserver/plane/app/views/issue/draft.py create mode 100644 apiserver/plane/app/views/issue/label.py create mode 100644 apiserver/plane/app/views/issue/link.py create mode 100644 apiserver/plane/app/views/issue/reaction.py create mode 100644 apiserver/plane/app/views/issue/relation.py create mode 100644 apiserver/plane/app/views/issue/sub_issue.py create mode 100644 apiserver/plane/app/views/issue/subscriber.py rename apiserver/plane/app/views/{module.py => module/base.py} (67%) create mode 100644 apiserver/plane/app/views/module/issue.py rename apiserver/plane/app/views/{notification.py => notification/base.py} (99%) rename apiserver/plane/app/views/{page.py => page/base.py} (99%) delete mode 100644 apiserver/plane/app/views/project.py create mode 100644 apiserver/plane/app/views/project/base.py create mode 100644 apiserver/plane/app/views/project/invite.py create mode 100644 apiserver/plane/app/views/project/member.py rename apiserver/plane/app/views/{state.py => state/base.py} (99%) rename apiserver/plane/app/views/{user.py => user/base.py} (100%) rename apiserver/plane/app/views/{view.py => view/base.py} (99%) rename apiserver/plane/app/views/{webhook.py => webhook/base.py} (99%) delete mode 100644 apiserver/plane/app/views/workspace.py create mode 100644 apiserver/plane/app/views/workspace/base.py create mode 100644 apiserver/plane/app/views/workspace/cycle.py create mode 100644 apiserver/plane/app/views/workspace/estimate.py create mode 100644 apiserver/plane/app/views/workspace/invite.py create mode 100644 apiserver/plane/app/views/workspace/label.py create mode 100644 apiserver/plane/app/views/workspace/member.py create mode 100644 apiserver/plane/app/views/workspace/module.py create mode 100644 apiserver/plane/app/views/workspace/state.py create mode 100644 apiserver/plane/app/views/workspace/user.py diff --git a/apiserver/plane/app/urls/issue.py b/apiserver/plane/app/urls/issue.py index 6b677287b..0d3b9e063 100644 --- a/apiserver/plane/app/urls/issue.py +++ b/apiserver/plane/app/urls/issue.py @@ -3,14 +3,15 @@ from django.urls import path from plane.app.views import ( BulkCreateIssueLabelsEndpoint, BulkDeleteIssuesEndpoint, + SubIssuesEndpoint, + IssueLinkViewSet, + IssueAttachmentEndpoint, CommentReactionViewSet, ExportIssuesEndpoint, IssueActivityEndpoint, IssueArchiveViewSet, - IssueAttachmentEndpoint, IssueCommentViewSet, IssueDraftViewSet, - IssueLinkViewSet, IssueListEndpoint, IssueReactionViewSet, IssueRelationViewSet, @@ -18,8 +19,6 @@ from plane.app.views import ( IssueUserDisplayPropertyEndpoint, IssueViewSet, LabelViewSet, - SubIssuesEndpoint, - UserWorkSpaceIssues, ) urlpatterns = [ @@ -82,11 +81,6 @@ urlpatterns = [ BulkDeleteIssuesEndpoint.as_view(), name="project-issues-bulk", ), - path( - "workspaces//my-issues/", - UserWorkSpaceIssues.as_view(), - name="workspace-issues", - ), ## path( "workspaces//projects//issues//sub-issues/", diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index dd668bd6e..bb5b7dd74 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -1,19 +1,26 @@ -from .project import ( +from .project.base import ( ProjectViewSet, - ProjectMemberViewSet, - UserProjectInvitationsViewset, - ProjectInvitationsViewset, - AddTeamToProjectEndpoint, ProjectIdentifierEndpoint, - ProjectJoinEndpoint, ProjectUserViewsEndpoint, - ProjectMemberUserEndpoint, ProjectFavoritesViewSet, ProjectPublicCoverImagesEndpoint, ProjectDeployBoardViewSet, +) + +from .project.invite import ( + UserProjectInvitationsViewset, + ProjectInvitationsViewset, + ProjectJoinEndpoint, +) + +from .project.member import ( + ProjectMemberViewSet, + AddTeamToProjectEndpoint, + ProjectMemberUserEndpoint, UserProjectRolesEndpoint, ) -from .user import ( + +from .user.base import ( UserEndpoint, UpdateUserOnBoardedEndpoint, UpdateUserTourCompletedEndpoint, @@ -24,71 +31,121 @@ from .oauth import OauthEndpoint from .base import BaseAPIView, BaseViewSet, WebhookMixin -from .workspace import ( +from .workspace.base import ( WorkSpaceViewSet, UserWorkSpacesEndpoint, WorkSpaceAvailabilityCheckEndpoint, - WorkspaceJoinEndpoint, - WorkSpaceMemberViewSet, - TeamMemberViewSet, - WorkspaceInvitationsViewset, - UserWorkspaceInvitationsViewSet, - UserLastProjectWithWorkspaceEndpoint, - WorkspaceMemberUserEndpoint, - WorkspaceMemberUserViewsEndpoint, - UserActivityGraphEndpoint, - UserIssueCompletedGraphEndpoint, UserWorkspaceDashboardEndpoint, WorkspaceThemeViewSet, - WorkspaceUserProfileStatsEndpoint, - WorkspaceUserActivityEndpoint, - WorkspaceUserProfileEndpoint, - WorkspaceUserProfileIssuesEndpoint, - WorkspaceLabelsEndpoint, + ExportWorkspaceUserActivityEndpoint +) + +from .workspace.member import ( + WorkSpaceMemberViewSet, + TeamMemberViewSet, + WorkspaceMemberUserEndpoint, WorkspaceProjectMemberEndpoint, - WorkspaceUserPropertiesEndpoint, + WorkspaceMemberUserViewsEndpoint, +) +from .workspace.invite import ( + WorkspaceInvitationsViewset, + WorkspaceJoinEndpoint, + UserWorkspaceInvitationsViewSet, +) +from .workspace.label import ( + WorkspaceLabelsEndpoint, +) +from .workspace.state import ( WorkspaceStatesEndpoint, +) +from .workspace.user import ( + UserLastProjectWithWorkspaceEndpoint, + WorkspaceUserProfileIssuesEndpoint, + WorkspaceUserPropertiesEndpoint, + WorkspaceUserProfileEndpoint, + WorkspaceUserActivityEndpoint, + WorkspaceUserProfileStatsEndpoint, + UserActivityGraphEndpoint, + UserIssueCompletedGraphEndpoint, +) +from .workspace.estimate import ( WorkspaceEstimatesEndpoint, - ExportWorkspaceUserActivityEndpoint, +) +from .workspace.module import ( WorkspaceModulesEndpoint, +) +from .workspace.cycle import ( WorkspaceCyclesEndpoint, ) -from .state import StateViewSet -from .view import ( + +from .state.base import StateViewSet +from .view.base import ( GlobalViewViewSet, GlobalViewIssuesViewSet, IssueViewViewSet, IssueViewFavoriteViewSet, ) -from .cycle import ( +from .cycle.base import ( CycleViewSet, - CycleIssueViewSet, CycleDateCheckEndpoint, CycleFavoriteViewSet, TransferCycleIssueEndpoint, CycleUserPropertiesEndpoint, ) -from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet -from .issue import ( +from .cycle.issue import ( + CycleIssueViewSet, +) + +from .asset.base import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet +from .issue.base import ( IssueListEndpoint, IssueViewSet, - WorkSpaceIssuesEndpoint, - IssueActivityEndpoint, - IssueCommentViewSet, IssueUserDisplayPropertyEndpoint, - LabelViewSet, BulkDeleteIssuesEndpoint, - UserWorkSpaceIssues, - SubIssuesEndpoint, - IssueLinkViewSet, - BulkCreateIssueLabelsEndpoint, - IssueAttachmentEndpoint, +) + +from .issue.activity import ( + IssueActivityEndpoint, +) + +from .issue.archive import ( IssueArchiveViewSet, - IssueSubscriberViewSet, +) + +from .issue.attachment import ( + IssueAttachmentEndpoint, +) + +from .issue.comment import ( + IssueCommentViewSet, CommentReactionViewSet, - IssueReactionViewSet, +) + +from .issue.draft import IssueDraftViewSet + +from .issue.label import ( + LabelViewSet, + BulkCreateIssueLabelsEndpoint, +) + +from .issue.link import ( + IssueLinkViewSet, +) + +from .issue.relation import ( IssueRelationViewSet, - IssueDraftViewSet, +) + +from .issue.reaction import ( + IssueReactionViewSet, +) + +from .issue.sub_issue import ( + SubIssuesEndpoint, +) + +from .issue.subscriber import ( + IssueSubscriberViewSet, ) from .auth_extended import ( @@ -107,17 +164,21 @@ from .authentication import ( MagicSignInEndpoint, ) -from .module import ( +from .module.base import ( ModuleViewSet, - ModuleIssueViewSet, ModuleLinkViewSet, ModuleFavoriteViewSet, ModuleUserPropertiesEndpoint, ) +from .module.issue import ( + ModuleIssueViewSet, +) + from .api import ApiTokenEndpoint -from .page import ( + +from .page.base import ( PageViewSet, PageFavoriteViewSet, PageLogEndpoint, @@ -127,19 +188,19 @@ from .page import ( from .search import GlobalSearchEndpoint, IssueSearchEndpoint -from .external import ( +from .external.base import ( GPTIntegrationEndpoint, UnsplashEndpoint, ) -from .estimate import ( +from .estimate.base import ( ProjectEstimatePointEndpoint, BulkEstimatePointEndpoint, ) -from .inbox import InboxViewSet, InboxIssueViewSet +from .inbox.base import InboxViewSet, InboxIssueViewSet -from .analytic import ( +from .analytic.base import ( AnalyticsEndpoint, AnalyticViewViewset, SavedAnalyticEndpoint, @@ -147,23 +208,23 @@ from .analytic import ( DefaultAnalyticsEndpoint, ) -from .notification import ( +from .notification.base import ( NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet, UserNotificationPreferenceEndpoint, ) -from .exporter import ExportIssuesEndpoint +from .exporter.base import ExportIssuesEndpoint from .config import ConfigurationEndpoint, MobileConfigurationEndpoint -from .webhook import ( +from .webhook.base import ( WebhookEndpoint, WebhookLogsEndpoint, WebhookSecretRegenerateEndpoint, ) -from .dashboard import DashboardEndpoint, WidgetsEndpoint +from .dashboard.base import DashboardEndpoint, WidgetsEndpoint from .error_404 import custom_404_view diff --git a/apiserver/plane/app/views/analytic.py b/apiserver/plane/app/views/analytic/base.py similarity index 100% rename from apiserver/plane/app/views/analytic.py rename to apiserver/plane/app/views/analytic/base.py diff --git a/apiserver/plane/app/views/asset.py b/apiserver/plane/app/views/asset/base.py similarity index 98% rename from apiserver/plane/app/views/asset.py rename to apiserver/plane/app/views/asset/base.py index fb5590610..6de4a4ee7 100644 --- a/apiserver/plane/app/views/asset.py +++ b/apiserver/plane/app/views/asset/base.py @@ -4,7 +4,7 @@ from rest_framework.response import Response from rest_framework.parsers import MultiPartParser, FormParser, JSONParser # Module imports -from .base import BaseAPIView, BaseViewSet +from ..base import BaseAPIView, BaseViewSet from plane.db.models import FileAsset, Workspace from plane.app.serializers import FileAssetSerializer diff --git a/apiserver/plane/app/views/cycle.py b/apiserver/plane/app/views/cycle/base.py similarity index 78% rename from apiserver/plane/app/views/cycle.py rename to apiserver/plane/app/views/cycle/base.py index 586da053b..9dc25474f 100644 --- a/apiserver/plane/app/views/cycle.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -29,7 +29,7 @@ from rest_framework.response import Response from rest_framework import status # Module imports -from . import BaseViewSet, BaseAPIView, WebhookMixin +from .. import BaseViewSet, BaseAPIView, WebhookMixin from plane.app.serializers import ( CycleSerializer, CycleIssueSerializer, @@ -660,278 +660,6 @@ class CycleViewSet(WebhookMixin, BaseViewSet): return Response(status=status.HTTP_204_NO_CONTENT) -class CycleIssueViewSet(WebhookMixin, BaseViewSet): - serializer_class = CycleIssueSerializer - model = CycleIssue - - webhook_event = "cycle_issue" - bulk = True - - permission_classes = [ - ProjectEntityPermission, - ] - - filterset_fields = [ - "issue__labels__id", - "issue__assignees__id", - ] - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("issue_id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - .filter(cycle_id=self.kwargs.get("cycle_id")) - .select_related("project") - .select_related("workspace") - .select_related("cycle") - .select_related("issue", "issue__state", "issue__project") - .prefetch_related("issue__assignees", "issue__labels") - .distinct() - ) - - @method_decorator(gzip_page) - def list(self, request, slug, project_id, cycle_id): - fields = [ - field - for field in request.GET.get("fields", "").split(",") - if field - ] - order_by = request.GET.get("order_by", "created_at") - filters = issue_filters(request.query_params, "GET") - queryset = ( - Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id) - .filter(project_id=project_id) - .filter(workspace__slug=slug) - .filter(**filters) - .select_related("workspace", "project", "state", "parent") - .prefetch_related( - "assignees", - "labels", - "issue_module__module", - "issue_cycle__cycle", - ) - .order_by(order_by) - .filter(**filters) - .annotate(cycle_id=F("issue_cycle__cycle_id")) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=~Q(labels__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=~Q(assignees__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "issue_module__module_id", - distinct=True, - filter=~Q(issue_module__module_id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) - .order_by(order_by) - ) - if self.fields: - issues = IssueSerializer( - queryset, many=True, fields=fields if fields else None - ).data - else: - issues = queryset.values( - "id", - "name", - "state_id", - "sort_order", - "completed_at", - "estimate_point", - "priority", - "start_date", - "target_date", - "sequence_id", - "project_id", - "parent_id", - "cycle_id", - "module_ids", - "label_ids", - "assignee_ids", - "sub_issues_count", - "created_at", - "updated_at", - "created_by", - "updated_by", - "attachment_count", - "link_count", - "is_draft", - "archived_at", - ) - return Response(issues, status=status.HTTP_200_OK) - - def create(self, request, slug, project_id, cycle_id): - issues = request.data.get("issues", []) - - if not issues: - return Response( - {"error": "Issues are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - cycle = Cycle.objects.get( - workspace__slug=slug, project_id=project_id, pk=cycle_id - ) - - if ( - cycle.end_date is not None - and cycle.end_date < timezone.now().date() - ): - return Response( - { - "error": "The Cycle has already been completed so no new issues can be added" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Get all CycleIssues already created - cycle_issues = list( - CycleIssue.objects.filter( - ~Q(cycle_id=cycle_id), issue_id__in=issues - ) - ) - existing_issues = [ - str(cycle_issue.issue_id) for cycle_issue in cycle_issues - ] - new_issues = list(set(issues) - set(existing_issues)) - - # New issues to create - created_records = CycleIssue.objects.bulk_create( - [ - CycleIssue( - project_id=project_id, - workspace_id=cycle.workspace_id, - created_by_id=request.user.id, - updated_by_id=request.user.id, - cycle_id=cycle_id, - issue_id=issue, - ) - for issue in new_issues - ], - batch_size=10, - ) - - # Updated Issues - updated_records = [] - update_cycle_issue_activity = [] - # Iterate over each cycle_issue in cycle_issues - for cycle_issue in cycle_issues: - # Update the cycle_issue's cycle_id - cycle_issue.cycle_id = cycle_id - # Add the modified cycle_issue to the records_to_update list - updated_records.append(cycle_issue) - # Record the update activity - update_cycle_issue_activity.append( - { - "old_cycle_id": str(cycle_issue.cycle_id), - "new_cycle_id": str(cycle_id), - "issue_id": str(cycle_issue.issue_id), - } - ) - - # Update the cycle issues - CycleIssue.objects.bulk_update( - updated_records, ["cycle_id"], batch_size=100 - ) - # Capture Issue Activity - issue_activity.delay( - type="cycle.activity.created", - requested_data=json.dumps({"cycles_list": issues}), - actor_id=str(self.request.user.id), - issue_id=None, - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - { - "updated_cycle_issues": update_cycle_issue_activity, - "created_cycle_issues": serializers.serialize( - "json", created_records - ), - } - ), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response({"message": "success"}, status=status.HTTP_201_CREATED) - - def destroy(self, request, slug, project_id, cycle_id, issue_id): - cycle_issue = CycleIssue.objects.get( - issue_id=issue_id, - workspace__slug=slug, - project_id=project_id, - cycle_id=cycle_id, - ) - issue_activity.delay( - type="cycle.activity.deleted", - requested_data=json.dumps( - { - "cycle_id": str(self.kwargs.get("cycle_id")), - "issues": [str(issue_id)], - } - ), - actor_id=str(self.request.user.id), - issue_id=str(issue_id), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - cycle_issue.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - class CycleDateCheckEndpoint(BaseAPIView): permission_classes = [ ProjectEntityPermission, diff --git a/apiserver/plane/app/views/cycle/issue.py b/apiserver/plane/app/views/cycle/issue.py new file mode 100644 index 000000000..84af4ff32 --- /dev/null +++ b/apiserver/plane/app/views/cycle/issue.py @@ -0,0 +1,312 @@ +# Python imports +import json + +# Django imports +from django.db.models import ( + Func, + F, + Q, + OuterRef, + Value, + UUIDField, +) +from django.core import serializers +from django.utils import timezone +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.db.models.functions import Coalesce + +# Third party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet, WebhookMixin +from plane.app.serializers import ( + IssueSerializer, + CycleIssueSerializer, +) +from plane.app.permissions import ProjectEntityPermission +from plane.db.models import ( + Cycle, + CycleIssue, + Issue, + IssueLink, + IssueAttachment, +) +from plane.bgtasks.issue_activites_task import issue_activity +from plane.utils.issue_filters import issue_filters + + +class CycleIssueViewSet(WebhookMixin, BaseViewSet): + serializer_class = CycleIssueSerializer + model = CycleIssue + + webhook_event = "cycle_issue" + bulk = True + + permission_classes = [ + ProjectEntityPermission, + ] + + filterset_fields = [ + "issue__labels__id", + "issue__assignees__id", + ] + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("issue_id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) + .filter(cycle_id=self.kwargs.get("cycle_id")) + .select_related("project") + .select_related("workspace") + .select_related("cycle") + .select_related("issue", "issue__state", "issue__project") + .prefetch_related("issue__assignees", "issue__labels") + .distinct() + ) + + @method_decorator(gzip_page) + def list(self, request, slug, project_id, cycle_id): + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] + order_by = request.GET.get("order_by", "created_at") + filters = issue_filters(request.query_params, "GET") + queryset = ( + Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id) + .filter(project_id=project_id) + .filter(workspace__slug=slug) + .filter(**filters) + .select_related("workspace", "project", "state", "parent") + .prefetch_related( + "assignees", + "labels", + "issue_module__module", + "issue_cycle__cycle", + ) + .order_by(order_by) + .filter(**filters) + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + .order_by(order_by) + ) + if self.fields: + issues = IssueSerializer( + queryset, many=True, fields=fields if fields else None + ).data + else: + issues = queryset.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + return Response(issues, status=status.HTTP_200_OK) + + def create(self, request, slug, project_id, cycle_id): + issues = request.data.get("issues", []) + + if not issues: + return Response( + {"error": "Issues are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + cycle = Cycle.objects.get( + workspace__slug=slug, project_id=project_id, pk=cycle_id + ) + + if ( + cycle.end_date is not None + and cycle.end_date < timezone.now().date() + ): + return Response( + { + "error": "The Cycle has already been completed so no new issues can be added" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Get all CycleIssues already created + cycle_issues = list( + CycleIssue.objects.filter( + ~Q(cycle_id=cycle_id), issue_id__in=issues + ) + ) + existing_issues = [ + str(cycle_issue.issue_id) for cycle_issue in cycle_issues + ] + new_issues = list(set(issues) - set(existing_issues)) + + # New issues to create + created_records = CycleIssue.objects.bulk_create( + [ + CycleIssue( + project_id=project_id, + workspace_id=cycle.workspace_id, + created_by_id=request.user.id, + updated_by_id=request.user.id, + cycle_id=cycle_id, + issue_id=issue, + ) + for issue in new_issues + ], + batch_size=10, + ) + + # Updated Issues + updated_records = [] + update_cycle_issue_activity = [] + # Iterate over each cycle_issue in cycle_issues + for cycle_issue in cycle_issues: + # Update the cycle_issue's cycle_id + cycle_issue.cycle_id = cycle_id + # Add the modified cycle_issue to the records_to_update list + updated_records.append(cycle_issue) + # Record the update activity + update_cycle_issue_activity.append( + { + "old_cycle_id": str(cycle_issue.cycle_id), + "new_cycle_id": str(cycle_id), + "issue_id": str(cycle_issue.issue_id), + } + ) + + # Update the cycle issues + CycleIssue.objects.bulk_update( + updated_records, ["cycle_id"], batch_size=100 + ) + # Capture Issue Activity + issue_activity.delay( + type="cycle.activity.created", + requested_data=json.dumps({"cycles_list": issues}), + actor_id=str(self.request.user.id), + issue_id=None, + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "updated_cycle_issues": update_cycle_issue_activity, + "created_cycle_issues": serializers.serialize( + "json", created_records + ), + } + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response({"message": "success"}, status=status.HTTP_201_CREATED) + + def destroy(self, request, slug, project_id, cycle_id, issue_id): + cycle_issue = CycleIssue.objects.get( + issue_id=issue_id, + workspace__slug=slug, + project_id=project_id, + cycle_id=cycle_id, + ) + issue_activity.delay( + type="cycle.activity.deleted", + requested_data=json.dumps( + { + "cycle_id": str(self.kwargs.get("cycle_id")), + "issues": [str(issue_id)], + } + ), + actor_id=str(self.request.user.id), + issue_id=str(issue_id), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + cycle_issue.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/dashboard.py b/apiserver/plane/app/views/dashboard/base.py similarity index 99% rename from apiserver/plane/app/views/dashboard.py rename to apiserver/plane/app/views/dashboard/base.py index 144ae74a9..27e45f59c 100644 --- a/apiserver/plane/app/views/dashboard.py +++ b/apiserver/plane/app/views/dashboard/base.py @@ -26,7 +26,7 @@ from rest_framework.response import Response from rest_framework import status # Module imports -from . import BaseAPIView +from .. import BaseAPIView from plane.db.models import ( Issue, IssueActivity, diff --git a/apiserver/plane/app/views/estimate.py b/apiserver/plane/app/views/estimate/base.py similarity index 99% rename from apiserver/plane/app/views/estimate.py rename to apiserver/plane/app/views/estimate/base.py index eae2e3351..7ac3035a9 100644 --- a/apiserver/plane/app/views/estimate.py +++ b/apiserver/plane/app/views/estimate/base.py @@ -3,7 +3,7 @@ from rest_framework.response import Response from rest_framework import status # Module imports -from .base import BaseViewSet, BaseAPIView +from ..base import BaseViewSet, BaseAPIView from plane.app.permissions import ProjectEntityPermission from plane.db.models import Project, Estimate, EstimatePoint from plane.app.serializers import ( diff --git a/apiserver/plane/app/views/exporter.py b/apiserver/plane/app/views/exporter/base.py similarity index 99% rename from apiserver/plane/app/views/exporter.py rename to apiserver/plane/app/views/exporter/base.py index 4e2d0760a..846508515 100644 --- a/apiserver/plane/app/views/exporter.py +++ b/apiserver/plane/app/views/exporter/base.py @@ -3,7 +3,7 @@ from rest_framework.response import Response from rest_framework import status # Module imports -from . import BaseAPIView +from .. import BaseAPIView from plane.app.permissions import WorkSpaceAdminPermission from plane.bgtasks.export_task import issue_export_task from plane.db.models import Project, ExporterHistory, Workspace diff --git a/apiserver/plane/app/views/external.py b/apiserver/plane/app/views/external/base.py similarity index 99% rename from apiserver/plane/app/views/external.py rename to apiserver/plane/app/views/external/base.py index 66667fe56..2d5d2c7aa 100644 --- a/apiserver/plane/app/views/external.py +++ b/apiserver/plane/app/views/external/base.py @@ -10,7 +10,7 @@ from rest_framework import status # Django imports # Module imports -from .base import BaseAPIView +from ..base import BaseAPIView from plane.app.permissions import ProjectEntityPermission from plane.db.models import Workspace, Project from plane.app.serializers import ( diff --git a/apiserver/plane/app/views/inbox.py b/apiserver/plane/app/views/inbox/base.py similarity index 99% rename from apiserver/plane/app/views/inbox.py rename to apiserver/plane/app/views/inbox/base.py index dad337bab..fb3b9227f 100644 --- a/apiserver/plane/app/views/inbox.py +++ b/apiserver/plane/app/views/inbox/base.py @@ -15,7 +15,7 @@ from rest_framework import status from rest_framework.response import Response # Module imports -from .base import BaseViewSet +from ..base import BaseViewSet from plane.app.permissions import ProjectBasePermission, ProjectLitePermission from plane.db.models import ( Inbox, diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py deleted file mode 100644 index fc05343de..000000000 --- a/apiserver/plane/app/views/issue.py +++ /dev/null @@ -1,2462 +0,0 @@ -# Python imports -import json -import random -from itertools import chain - -# Django imports -from django.utils import timezone -from django.db.models import ( - Prefetch, - OuterRef, - Func, - F, - Q, - Case, - Value, - CharField, - When, - Exists, - Max, -) -from django.core.serializers.json import DjangoJSONEncoder -from django.utils.decorators import method_decorator -from django.views.decorators.gzip import gzip_page -from django.db import IntegrityError -from django.contrib.postgres.aggregates import ArrayAgg -from django.contrib.postgres.fields import ArrayField -from django.db.models import UUIDField -from django.db.models.functions import Coalesce - -# Third Party imports -from rest_framework.response import Response -from rest_framework import status -from rest_framework.parsers import MultiPartParser, FormParser - -# Module imports -from . import BaseViewSet, BaseAPIView, WebhookMixin -from plane.app.serializers import ( - IssueActivitySerializer, - IssueCommentSerializer, - IssuePropertySerializer, - IssueSerializer, - IssueCreateSerializer, - LabelSerializer, - IssueFlatSerializer, - IssueLinkSerializer, - IssueLiteSerializer, - IssueAttachmentSerializer, - IssueSubscriberSerializer, - ProjectMemberLiteSerializer, - IssueReactionSerializer, - CommentReactionSerializer, - IssueRelationSerializer, - RelatedIssueSerializer, - IssueDetailSerializer, -) -from plane.app.permissions import ( - ProjectEntityPermission, - WorkSpaceAdminPermission, - ProjectMemberPermission, - ProjectLitePermission, -) -from plane.db.models import ( - Project, - Issue, - IssueActivity, - IssueComment, - IssueProperty, - Label, - IssueLink, - IssueAttachment, - IssueSubscriber, - ProjectMember, - IssueReaction, - CommentReaction, - IssueRelation, -) -from plane.bgtasks.issue_activites_task import issue_activity -from plane.utils.grouper import group_results -from plane.utils.issue_filters import issue_filters -from collections import defaultdict -from plane.utils.cache import invalidate_cache - - -class IssueListEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - def get(self, request, slug, project_id): - issue_ids = request.GET.get("issues", False) - - if not issue_ids: - return Response( - {"error": "Issues are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - issue_ids = [ - issue_id for issue_id in issue_ids.split(",") if issue_id != "" - ] - - queryset = ( - Issue.issue_objects.filter( - workspace__slug=slug, project_id=project_id, pk__in=issue_ids - ) - .filter(workspace__slug=self.kwargs.get("slug")) - .select_related("workspace", "project", "state", "parent") - .prefetch_related("assignees", "labels", "issue_module__module") - .annotate(cycle_id=F("issue_cycle__cycle_id")) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=~Q(labels__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=~Q(assignees__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "issue_module__module_id", - distinct=True, - filter=~Q(issue_module__module_id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) - ).distinct() - - filters = issue_filters(request.query_params, "GET") - - # Custom ordering for priority and state - priority_order = ["urgent", "high", "medium", "low", "none"] - state_order = [ - "backlog", - "unstarted", - "started", - "completed", - "cancelled", - ] - - order_by_param = request.GET.get("order_by", "-created_at") - - issue_queryset = queryset.filter(**filters) - - # Priority Ordering - if order_by_param == "priority" or order_by_param == "-priority": - priority_order = ( - priority_order - if order_by_param == "priority" - else priority_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - priority_order=Case( - *[ - When(priority=p, then=Value(i)) - for i, p in enumerate(priority_order) - ], - output_field=CharField(), - ) - ).order_by("priority_order") - - # State Ordering - elif order_by_param in [ - "state__name", - "state__group", - "-state__name", - "-state__group", - ]: - state_order = ( - state_order - if order_by_param in ["state__name", "state__group"] - else state_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - state_order=Case( - *[ - When(state__group=state_group, then=Value(i)) - for i, state_group in enumerate(state_order) - ], - default=Value(len(state_order)), - output_field=CharField(), - ) - ).order_by("state_order") - # assignee and label ordering - elif order_by_param in [ - "labels__name", - "-labels__name", - "assignees__first_name", - "-assignees__first_name", - ]: - issue_queryset = issue_queryset.annotate( - max_values=Max( - order_by_param[1::] - if order_by_param.startswith("-") - else order_by_param - ) - ).order_by( - "-max_values" - if order_by_param.startswith("-") - else "max_values" - ) - else: - issue_queryset = issue_queryset.order_by(order_by_param) - - if self.fields or self.expand: - issues = IssueSerializer( - queryset, many=True, fields=self.fields, expand=self.expand - ).data - else: - issues = issue_queryset.values( - "id", - "name", - "state_id", - "sort_order", - "completed_at", - "estimate_point", - "priority", - "start_date", - "target_date", - "sequence_id", - "project_id", - "parent_id", - "cycle_id", - "module_ids", - "label_ids", - "assignee_ids", - "sub_issues_count", - "created_at", - "updated_at", - "created_by", - "updated_by", - "attachment_count", - "link_count", - "is_draft", - "archived_at", - ) - return Response(issues, status=status.HTTP_200_OK) - - -class IssueViewSet(WebhookMixin, BaseViewSet): - def get_serializer_class(self): - return ( - IssueCreateSerializer - if self.action in ["create", "update", "partial_update"] - else IssueSerializer - ) - - model = Issue - webhook_event = "issue" - permission_classes = [ - ProjectEntityPermission, - ] - - search_fields = [ - "name", - ] - - filterset_fields = [ - "state__name", - "assignees__id", - "workspace__id", - ] - - def get_queryset(self): - return ( - Issue.issue_objects.filter( - project_id=self.kwargs.get("project_id") - ) - .filter(workspace__slug=self.kwargs.get("slug")) - .select_related("workspace", "project", "state", "parent") - .prefetch_related("assignees", "labels", "issue_module__module") - .annotate(cycle_id=F("issue_cycle__cycle_id")) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=~Q(labels__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=~Q(assignees__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "issue_module__module_id", - distinct=True, - filter=~Q(issue_module__module_id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) - ).distinct() - - @method_decorator(gzip_page) - def list(self, request, slug, project_id): - filters = issue_filters(request.query_params, "GET") - order_by_param = request.GET.get("order_by", "-created_at") - - issue_queryset = self.get_queryset().filter(**filters) - # Custom ordering for priority and state - priority_order = ["urgent", "high", "medium", "low", "none"] - state_order = [ - "backlog", - "unstarted", - "started", - "completed", - "cancelled", - ] - - # Priority Ordering - if order_by_param == "priority" or order_by_param == "-priority": - priority_order = ( - priority_order - if order_by_param == "priority" - else priority_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - priority_order=Case( - *[ - When(priority=p, then=Value(i)) - for i, p in enumerate(priority_order) - ], - output_field=CharField(), - ) - ).order_by("priority_order") - - # State Ordering - elif order_by_param in [ - "state__name", - "state__group", - "-state__name", - "-state__group", - ]: - state_order = ( - state_order - if order_by_param in ["state__name", "state__group"] - else state_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - state_order=Case( - *[ - When(state__group=state_group, then=Value(i)) - for i, state_group in enumerate(state_order) - ], - default=Value(len(state_order)), - output_field=CharField(), - ) - ).order_by("state_order") - # assignee and label ordering - elif order_by_param in [ - "labels__name", - "-labels__name", - "assignees__first_name", - "-assignees__first_name", - ]: - issue_queryset = issue_queryset.annotate( - max_values=Max( - order_by_param[1::] - if order_by_param.startswith("-") - else order_by_param - ) - ).order_by( - "-max_values" - if order_by_param.startswith("-") - else "max_values" - ) - else: - issue_queryset = issue_queryset.order_by(order_by_param) - - # Only use serializer when expand or fields else return by values - if self.expand or self.fields: - issues = IssueSerializer( - issue_queryset, - many=True, - fields=self.fields, - expand=self.expand, - ).data - else: - issues = issue_queryset.values( - "id", - "name", - "state_id", - "sort_order", - "completed_at", - "estimate_point", - "priority", - "start_date", - "target_date", - "sequence_id", - "project_id", - "parent_id", - "cycle_id", - "module_ids", - "label_ids", - "assignee_ids", - "sub_issues_count", - "created_at", - "updated_at", - "created_by", - "updated_by", - "attachment_count", - "link_count", - "is_draft", - "archived_at", - ) - return Response(issues, status=status.HTTP_200_OK) - - def create(self, request, slug, project_id): - project = Project.objects.get(pk=project_id) - - serializer = IssueCreateSerializer( - data=request.data, - context={ - "project_id": project_id, - "workspace_id": project.workspace_id, - "default_assignee_id": project.default_assignee_id, - }, - ) - - if serializer.is_valid(): - serializer.save() - - # Track the issue - issue_activity.delay( - type="issue.activity.created", - requested_data=json.dumps( - self.request.data, cls=DjangoJSONEncoder - ), - actor_id=str(request.user.id), - issue_id=str(serializer.data.get("id", None)), - project_id=str(project_id), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - issue = ( - self.get_queryset() - .filter(pk=serializer.data["id"]) - .values( - "id", - "name", - "state_id", - "sort_order", - "completed_at", - "estimate_point", - "priority", - "start_date", - "target_date", - "sequence_id", - "project_id", - "parent_id", - "cycle_id", - "module_ids", - "label_ids", - "assignee_ids", - "sub_issues_count", - "created_at", - "updated_at", - "created_by", - "updated_by", - "attachment_count", - "link_count", - "is_draft", - "archived_at", - ) - .first() - ) - return Response(issue, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def retrieve(self, request, slug, project_id, pk=None): - issue = ( - self.get_queryset() - .filter(pk=pk) - .prefetch_related( - Prefetch( - "issue_reactions", - queryset=IssueReaction.objects.select_related( - "issue", "actor" - ), - ) - ) - .prefetch_related( - Prefetch( - "issue_attachment", - queryset=IssueAttachment.objects.select_related("issue"), - ) - ) - .prefetch_related( - Prefetch( - "issue_link", - queryset=IssueLink.objects.select_related("created_by"), - ) - ) - .annotate( - is_subscribed=Exists( - IssueSubscriber.objects.filter( - workspace__slug=slug, - project_id=project_id, - issue_id=OuterRef("pk"), - subscriber=request.user, - ) - ) - ) - ).first() - if not issue: - return Response( - {"error": "The required object does not exist."}, - status=status.HTTP_404_NOT_FOUND, - ) - - serializer = IssueDetailSerializer(issue, expand=self.expand) - return Response(serializer.data, status=status.HTTP_200_OK) - - def partial_update(self, request, slug, project_id, pk=None): - issue = self.get_queryset().filter(pk=pk).first() - - if not issue: - return Response( - {"error": "Issue not found"}, - status=status.HTTP_404_NOT_FOUND, - ) - - current_instance = json.dumps( - IssueSerializer(issue).data, cls=DjangoJSONEncoder - ) - - requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) - serializer = IssueCreateSerializer( - issue, data=request.data, partial=True - ) - if serializer.is_valid(): - serializer.save() - issue_activity.delay( - type="issue.activity.updated", - requested_data=requested_data, - actor_id=str(request.user.id), - issue_id=str(pk), - project_id=str(project_id), - current_instance=current_instance, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - issue = self.get_queryset().filter(pk=pk).first() - return Response(status=status.HTTP_204_NO_CONTENT) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, slug, project_id, pk=None): - issue = Issue.objects.get( - workspace__slug=slug, project_id=project_id, pk=pk - ) - issue.delete() - issue_activity.delay( - type="issue.activity.deleted", - requested_data=json.dumps({"issue_id": str(pk)}), - actor_id=str(request.user.id), - issue_id=str(pk), - project_id=str(project_id), - current_instance={}, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(status=status.HTTP_204_NO_CONTENT) - - -# TODO: deprecated remove once confirmed -class UserWorkSpaceIssues(BaseAPIView): - @method_decorator(gzip_page) - def get(self, request, slug): - filters = issue_filters(request.query_params, "GET") - # Custom ordering for priority and state - priority_order = ["urgent", "high", "medium", "low", "none"] - state_order = [ - "backlog", - "unstarted", - "started", - "completed", - "cancelled", - ] - - order_by_param = request.GET.get("order_by", "-created_at") - - issue_queryset = ( - Issue.issue_objects.filter( - ( - Q(assignees__in=[request.user]) - | Q(created_by=request.user) - | Q(issue_subscribers__subscriber=request.user) - ), - workspace__slug=slug, - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .select_related("project") - .select_related("workspace") - .select_related("state") - .select_related("parent") - .prefetch_related("assignees") - .prefetch_related("labels") - .order_by(order_by_param) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .filter(**filters) - ).distinct() - - # Priority Ordering - if order_by_param == "priority" or order_by_param == "-priority": - priority_order = ( - priority_order - if order_by_param == "priority" - else priority_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - priority_order=Case( - *[ - When(priority=p, then=Value(i)) - for i, p in enumerate(priority_order) - ], - output_field=CharField(), - ) - ).order_by("priority_order") - - # State Ordering - elif order_by_param in [ - "state__name", - "state__group", - "-state__name", - "-state__group", - ]: - state_order = ( - state_order - if order_by_param in ["state__name", "state__group"] - else state_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - state_order=Case( - *[ - When(state__group=state_group, then=Value(i)) - for i, state_group in enumerate(state_order) - ], - default=Value(len(state_order)), - output_field=CharField(), - ) - ).order_by("state_order") - # assignee and label ordering - elif order_by_param in [ - "labels__name", - "-labels__name", - "assignees__first_name", - "-assignees__first_name", - ]: - issue_queryset = issue_queryset.annotate( - max_values=Max( - order_by_param[1::] - if order_by_param.startswith("-") - else order_by_param - ) - ).order_by( - "-max_values" - if order_by_param.startswith("-") - else "max_values" - ) - else: - issue_queryset = issue_queryset.order_by(order_by_param) - - issues = IssueLiteSerializer(issue_queryset, many=True).data - - ## Grouping the results - group_by = request.GET.get("group_by", False) - sub_group_by = request.GET.get("sub_group_by", False) - if sub_group_by and sub_group_by == group_by: - return Response( - {"error": "Group by and sub group by cannot be same"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - if group_by: - grouped_results = group_results(issues, group_by, sub_group_by) - return Response( - grouped_results, - status=status.HTTP_200_OK, - ) - - return Response(issues, status=status.HTTP_200_OK) - - -# TODO: deprecated remove once confirmed -class WorkSpaceIssuesEndpoint(BaseAPIView): - permission_classes = [ - WorkSpaceAdminPermission, - ] - - @method_decorator(gzip_page) - def get(self, request, slug): - issues = ( - Issue.issue_objects.filter(workspace__slug=slug) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - .order_by("-created_at") - ) - serializer = IssueSerializer(issues, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class IssueActivityEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - @method_decorator(gzip_page) - def get(self, request, slug, project_id, issue_id): - filters = {} - if request.GET.get("created_at__gt", None) is not None: - filters = {"created_at__gt": request.GET.get("created_at__gt")} - - issue_activities = ( - IssueActivity.objects.filter(issue_id=issue_id) - .filter( - ~Q(field__in=["comment", "vote", "reaction", "draft"]), - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - workspace__slug=slug, - ) - .filter(**filters) - .select_related("actor", "workspace", "issue", "project") - ).order_by("created_at") - issue_comments = ( - IssueComment.objects.filter(issue_id=issue_id) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - workspace__slug=slug, - ) - .filter(**filters) - .order_by("created_at") - .select_related("actor", "issue", "project", "workspace") - .prefetch_related( - Prefetch( - "comment_reactions", - queryset=CommentReaction.objects.select_related("actor"), - ) - ) - ) - issue_activities = IssueActivitySerializer( - issue_activities, many=True - ).data - issue_comments = IssueCommentSerializer(issue_comments, many=True).data - - if request.GET.get("activity_type", None) == "issue-property": - return Response(issue_activities, status=status.HTTP_200_OK) - - if request.GET.get("activity_type", None) == "issue-comment": - return Response(issue_comments, status=status.HTTP_200_OK) - - result_list = sorted( - chain(issue_activities, issue_comments), - key=lambda instance: instance["created_at"], - ) - - return Response(result_list, status=status.HTTP_200_OK) - - -class IssueCommentViewSet(WebhookMixin, BaseViewSet): - serializer_class = IssueCommentSerializer - model = IssueComment - webhook_event = "issue_comment" - permission_classes = [ - ProjectLitePermission, - ] - - filterset_fields = [ - "issue__id", - "workspace__id", - ] - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter(issue_id=self.kwargs.get("issue_id")) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - .select_related("project") - .select_related("workspace") - .select_related("issue") - .annotate( - is_member=Exists( - ProjectMember.objects.filter( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), - member_id=self.request.user.id, - is_active=True, - ) - ) - ) - .distinct() - ) - - def create(self, request, slug, project_id, issue_id): - serializer = IssueCommentSerializer(data=request.data) - if serializer.is_valid(): - serializer.save( - project_id=project_id, - issue_id=issue_id, - actor=request.user, - ) - issue_activity.delay( - type="comment.activity.created", - requested_data=json.dumps( - serializer.data, cls=DjangoJSONEncoder - ), - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("issue_id")), - project_id=str(self.kwargs.get("project_id")), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def partial_update(self, request, slug, project_id, issue_id, pk): - issue_comment = IssueComment.objects.get( - workspace__slug=slug, - project_id=project_id, - issue_id=issue_id, - pk=pk, - ) - requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) - current_instance = json.dumps( - IssueCommentSerializer(issue_comment).data, - cls=DjangoJSONEncoder, - ) - serializer = IssueCommentSerializer( - issue_comment, data=request.data, partial=True - ) - if serializer.is_valid(): - serializer.save() - issue_activity.delay( - type="comment.activity.updated", - requested_data=requested_data, - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=current_instance, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, slug, project_id, issue_id, pk): - issue_comment = IssueComment.objects.get( - workspace__slug=slug, - project_id=project_id, - issue_id=issue_id, - pk=pk, - ) - current_instance = json.dumps( - IssueCommentSerializer(issue_comment).data, - cls=DjangoJSONEncoder, - ) - issue_comment.delete() - issue_activity.delay( - type="comment.activity.deleted", - requested_data=json.dumps({"comment_id": str(pk)}), - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=current_instance, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(status=status.HTTP_204_NO_CONTENT) - - -class IssueUserDisplayPropertyEndpoint(BaseAPIView): - permission_classes = [ - ProjectLitePermission, - ] - - def patch(self, request, slug, project_id): - issue_property = IssueProperty.objects.get( - user=request.user, - project_id=project_id, - ) - - issue_property.filters = request.data.get( - "filters", issue_property.filters - ) - issue_property.display_filters = request.data.get( - "display_filters", issue_property.display_filters - ) - issue_property.display_properties = request.data.get( - "display_properties", issue_property.display_properties - ) - issue_property.save() - serializer = IssuePropertySerializer(issue_property) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - def get(self, request, slug, project_id): - issue_property, _ = IssueProperty.objects.get_or_create( - user=request.user, project_id=project_id - ) - serializer = IssuePropertySerializer(issue_property) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class LabelViewSet(BaseViewSet): - serializer_class = LabelSerializer - model = Label - permission_classes = [ - ProjectMemberPermission, - ] - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter(project__project_projectmember__member=self.request.user) - .select_related("project") - .select_related("workspace") - .select_related("parent") - .distinct() - .order_by("sort_order") - ) - - @invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False) - def create(self, request, slug, project_id): - try: - serializer = LabelSerializer(data=request.data) - if serializer.is_valid(): - serializer.save(project_id=project_id) - return Response( - serializer.data, status=status.HTTP_201_CREATED - ) - return Response( - serializer.errors, status=status.HTTP_400_BAD_REQUEST - ) - except IntegrityError: - return Response( - { - "error": "Label with the same name already exists in the project" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - @invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False) - def partial_update(self, request, *args, **kwargs): - return super().partial_update(request, *args, **kwargs) - - @invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False) - def destroy(self, request, *args, **kwargs): - return super().destroy(request, *args, **kwargs) - - -class BulkDeleteIssuesEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - def delete(self, request, slug, project_id): - issue_ids = request.data.get("issue_ids", []) - - if not len(issue_ids): - return Response( - {"error": "Issue IDs are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - issues = Issue.issue_objects.filter( - workspace__slug=slug, project_id=project_id, pk__in=issue_ids - ) - - total_issues = len(issues) - - issues.delete() - - return Response( - {"message": f"{total_issues} issues were deleted"}, - status=status.HTTP_200_OK, - ) - - -class SubIssuesEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - @method_decorator(gzip_page) - def get(self, request, slug, project_id, issue_id): - sub_issues = ( - Issue.issue_objects.filter( - parent_id=issue_id, workspace__slug=slug - ) - .select_related("workspace", "project", "state", "parent") - .prefetch_related("assignees", "labels", "issue_module__module") - .annotate(cycle_id=F("issue_cycle__cycle_id")) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=~Q(labels__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=~Q(assignees__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "issue_module__module_id", - distinct=True, - filter=~Q(issue_module__module_id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) - .annotate(state_group=F("state__group")) - ) - - # create's a dict with state group name with their respective issue id's - result = defaultdict(list) - for sub_issue in sub_issues: - result[sub_issue.state_group].append(str(sub_issue.id)) - - sub_issues = sub_issues.values( - "id", - "name", - "state_id", - "sort_order", - "completed_at", - "estimate_point", - "priority", - "start_date", - "target_date", - "sequence_id", - "project_id", - "parent_id", - "cycle_id", - "module_ids", - "label_ids", - "assignee_ids", - "sub_issues_count", - "created_at", - "updated_at", - "created_by", - "updated_by", - "attachment_count", - "link_count", - "is_draft", - "archived_at", - ) - return Response( - { - "sub_issues": sub_issues, - "state_distribution": result, - }, - status=status.HTTP_200_OK, - ) - - # Assign multiple sub issues - def post(self, request, slug, project_id, issue_id): - parent_issue = Issue.issue_objects.get(pk=issue_id) - sub_issue_ids = request.data.get("sub_issue_ids", []) - - if not len(sub_issue_ids): - return Response( - {"error": "Sub Issue IDs are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids) - - for sub_issue in sub_issues: - sub_issue.parent = parent_issue - - _ = Issue.objects.bulk_update(sub_issues, ["parent"], batch_size=10) - - updated_sub_issues = Issue.issue_objects.filter( - id__in=sub_issue_ids - ).annotate(state_group=F("state__group")) - - # Track the issue - _ = [ - issue_activity.delay( - type="issue.activity.updated", - requested_data=json.dumps({"parent": str(issue_id)}), - actor_id=str(request.user.id), - issue_id=str(sub_issue_id), - project_id=str(project_id), - current_instance=json.dumps({"parent": str(sub_issue_id)}), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - for sub_issue_id in sub_issue_ids - ] - - # create's a dict with state group name with their respective issue id's - result = defaultdict(list) - for sub_issue in updated_sub_issues: - result[sub_issue.state_group].append(str(sub_issue.id)) - - serializer = IssueSerializer( - updated_sub_issues, - many=True, - ) - return Response( - { - "sub_issues": serializer.data, - "state_distribution": result, - }, - status=status.HTTP_200_OK, - ) - - -class IssueLinkViewSet(BaseViewSet): - permission_classes = [ - ProjectEntityPermission, - ] - - model = IssueLink - serializer_class = IssueLinkSerializer - - def get_queryset(self): - return ( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter(issue_id=self.kwargs.get("issue_id")) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - .order_by("-created_at") - .distinct() - ) - - def create(self, request, slug, project_id, issue_id): - serializer = IssueLinkSerializer(data=request.data) - if serializer.is_valid(): - serializer.save( - project_id=project_id, - issue_id=issue_id, - ) - issue_activity.delay( - type="link.activity.created", - requested_data=json.dumps( - serializer.data, cls=DjangoJSONEncoder - ), - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("issue_id")), - project_id=str(self.kwargs.get("project_id")), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def partial_update(self, request, slug, project_id, issue_id, pk): - issue_link = IssueLink.objects.get( - workspace__slug=slug, - project_id=project_id, - issue_id=issue_id, - pk=pk, - ) - requested_data = json.dumps(request.data, cls=DjangoJSONEncoder) - current_instance = json.dumps( - IssueLinkSerializer(issue_link).data, - cls=DjangoJSONEncoder, - ) - serializer = IssueLinkSerializer( - issue_link, data=request.data, partial=True - ) - if serializer.is_valid(): - serializer.save() - issue_activity.delay( - type="link.activity.updated", - requested_data=requested_data, - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=current_instance, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, slug, project_id, issue_id, pk): - issue_link = IssueLink.objects.get( - workspace__slug=slug, - project_id=project_id, - issue_id=issue_id, - pk=pk, - ) - current_instance = json.dumps( - IssueLinkSerializer(issue_link).data, - cls=DjangoJSONEncoder, - ) - issue_activity.delay( - type="link.activity.deleted", - requested_data=json.dumps({"link_id": str(pk)}), - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=current_instance, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - issue_link.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class BulkCreateIssueLabelsEndpoint(BaseAPIView): - def post(self, request, slug, project_id): - label_data = request.data.get("label_data", []) - project = Project.objects.get(pk=project_id) - - labels = Label.objects.bulk_create( - [ - Label( - name=label.get("name", "Migrated"), - description=label.get("description", "Migrated Issue"), - color="#" + "%06x" % random.randint(0, 0xFFFFFF), - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - updated_by=request.user, - ) - for label in label_data - ], - batch_size=50, - ignore_conflicts=True, - ) - - return Response( - {"labels": LabelSerializer(labels, many=True).data}, - status=status.HTTP_201_CREATED, - ) - - -class IssueAttachmentEndpoint(BaseAPIView): - serializer_class = IssueAttachmentSerializer - permission_classes = [ - ProjectEntityPermission, - ] - model = IssueAttachment - parser_classes = (MultiPartParser, FormParser) - - def post(self, request, slug, project_id, issue_id): - serializer = IssueAttachmentSerializer(data=request.data) - if serializer.is_valid(): - serializer.save(project_id=project_id, issue_id=issue_id) - issue_activity.delay( - type="attachment.activity.created", - requested_data=None, - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("issue_id", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - serializer.data, - cls=DjangoJSONEncoder, - ), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def delete(self, request, slug, project_id, issue_id, pk): - issue_attachment = IssueAttachment.objects.get(pk=pk) - issue_attachment.asset.delete(save=False) - issue_attachment.delete() - issue_activity.delay( - type="attachment.activity.deleted", - requested_data=None, - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("issue_id", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - - return Response(status=status.HTTP_204_NO_CONTENT) - - def get(self, request, slug, project_id, issue_id): - issue_attachments = IssueAttachment.objects.filter( - issue_id=issue_id, workspace__slug=slug, project_id=project_id - ) - serializer = IssueAttachmentSerializer(issue_attachments, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class IssueArchiveViewSet(BaseViewSet): - permission_classes = [ - ProjectEntityPermission, - ] - serializer_class = IssueFlatSerializer - model = Issue - - def get_queryset(self): - return ( - Issue.objects.annotate( - sub_issues_count=Issue.objects.filter(parent=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .filter(archived_at__isnull=False) - .filter(project_id=self.kwargs.get("project_id")) - .filter(workspace__slug=self.kwargs.get("slug")) - .select_related("workspace", "project", "state", "parent") - .prefetch_related("assignees", "labels", "issue_module__module") - .annotate(cycle_id=F("issue_cycle__cycle_id")) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=~Q(labels__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=~Q(assignees__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "issue_module__module_id", - distinct=True, - filter=~Q(issue_module__module_id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) - ) - - @method_decorator(gzip_page) - def list(self, request, slug, project_id): - filters = issue_filters(request.query_params, "GET") - show_sub_issues = request.GET.get("show_sub_issues", "true") - - # Custom ordering for priority and state - priority_order = ["urgent", "high", "medium", "low", "none"] - state_order = [ - "backlog", - "unstarted", - "started", - "completed", - "cancelled", - ] - - order_by_param = request.GET.get("order_by", "-created_at") - - issue_queryset = self.get_queryset().filter(**filters) - - # Priority Ordering - if order_by_param == "priority" or order_by_param == "-priority": - priority_order = ( - priority_order - if order_by_param == "priority" - else priority_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - priority_order=Case( - *[ - When(priority=p, then=Value(i)) - for i, p in enumerate(priority_order) - ], - output_field=CharField(), - ) - ).order_by("priority_order") - - # State Ordering - elif order_by_param in [ - "state__name", - "state__group", - "-state__name", - "-state__group", - ]: - state_order = ( - state_order - if order_by_param in ["state__name", "state__group"] - else state_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - state_order=Case( - *[ - When(state__group=state_group, then=Value(i)) - for i, state_group in enumerate(state_order) - ], - default=Value(len(state_order)), - output_field=CharField(), - ) - ).order_by("state_order") - # assignee and label ordering - elif order_by_param in [ - "labels__name", - "-labels__name", - "assignees__first_name", - "-assignees__first_name", - ]: - issue_queryset = issue_queryset.annotate( - max_values=Max( - order_by_param[1::] - if order_by_param.startswith("-") - else order_by_param - ) - ).order_by( - "-max_values" - if order_by_param.startswith("-") - else "max_values" - ) - else: - issue_queryset = issue_queryset.order_by(order_by_param) - - issue_queryset = ( - issue_queryset - if show_sub_issues == "true" - else issue_queryset.filter(parent__isnull=True) - ) - if self.expand or self.fields: - issues = IssueSerializer( - issue_queryset, - many=True, - fields=self.fields, - ).data - else: - issues = issue_queryset.values( - "id", - "name", - "state_id", - "sort_order", - "completed_at", - "estimate_point", - "priority", - "start_date", - "target_date", - "sequence_id", - "project_id", - "parent_id", - "cycle_id", - "module_ids", - "label_ids", - "assignee_ids", - "sub_issues_count", - "created_at", - "updated_at", - "created_by", - "updated_by", - "attachment_count", - "link_count", - "is_draft", - "archived_at", - ) - return Response(issues, status=status.HTTP_200_OK) - - def retrieve(self, request, slug, project_id, pk=None): - issue = ( - self.get_queryset() - .filter(pk=pk) - .prefetch_related( - Prefetch( - "issue_reactions", - queryset=IssueReaction.objects.select_related( - "issue", "actor" - ), - ) - ) - .prefetch_related( - Prefetch( - "issue_attachment", - queryset=IssueAttachment.objects.select_related("issue"), - ) - ) - .prefetch_related( - Prefetch( - "issue_link", - queryset=IssueLink.objects.select_related("created_by"), - ) - ) - .annotate( - is_subscribed=Exists( - IssueSubscriber.objects.filter( - workspace__slug=slug, - project_id=project_id, - issue_id=OuterRef("pk"), - subscriber=request.user, - ) - ) - ) - ).first() - if not issue: - return Response( - {"error": "The required object does not exist."}, - status=status.HTTP_404_NOT_FOUND, - ) - serializer = IssueDetailSerializer(issue, expand=self.expand) - return Response(serializer.data, status=status.HTTP_200_OK) - - def archive(self, request, slug, project_id, pk=None): - issue = Issue.issue_objects.get( - workspace__slug=slug, - project_id=project_id, - pk=pk, - ) - if issue.state.group not in ["completed", "cancelled"]: - return Response( - { - "error": "Can only archive completed or cancelled state group issue" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - issue_activity.delay( - type="issue.activity.updated", - requested_data=json.dumps( - { - "archived_at": str(timezone.now().date()), - "automation": False, - } - ), - actor_id=str(request.user.id), - issue_id=str(issue.id), - project_id=str(project_id), - current_instance=json.dumps( - IssueSerializer(issue).data, cls=DjangoJSONEncoder - ), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - issue.archived_at = timezone.now().date() - issue.save() - - return Response( - {"archived_at": str(issue.archived_at)}, status=status.HTTP_200_OK - ) - - def unarchive(self, request, slug, project_id, pk=None): - issue = Issue.objects.get( - workspace__slug=slug, - project_id=project_id, - archived_at__isnull=False, - pk=pk, - ) - issue_activity.delay( - type="issue.activity.updated", - requested_data=json.dumps({"archived_at": None}), - actor_id=str(request.user.id), - issue_id=str(issue.id), - project_id=str(project_id), - current_instance=json.dumps( - IssueSerializer(issue).data, cls=DjangoJSONEncoder - ), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - issue.archived_at = None - issue.save() - - return Response(status=status.HTTP_204_NO_CONTENT) - - -class IssueSubscriberViewSet(BaseViewSet): - serializer_class = IssueSubscriberSerializer - model = IssueSubscriber - - permission_classes = [ - ProjectEntityPermission, - ] - - def get_permissions(self): - if self.action in ["subscribe", "unsubscribe", "subscription_status"]: - self.permission_classes = [ - ProjectLitePermission, - ] - else: - self.permission_classes = [ - ProjectEntityPermission, - ] - - return super(IssueSubscriberViewSet, self).get_permissions() - - def perform_create(self, serializer): - serializer.save( - project_id=self.kwargs.get("project_id"), - issue_id=self.kwargs.get("issue_id"), - ) - - def get_queryset(self): - return ( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter(issue_id=self.kwargs.get("issue_id")) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - .order_by("-created_at") - .distinct() - ) - - def list(self, request, slug, project_id, issue_id): - members = ProjectMember.objects.filter( - workspace__slug=slug, - project_id=project_id, - is_active=True, - ).select_related("member") - serializer = ProjectMemberLiteSerializer(members, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - - def destroy(self, request, slug, project_id, issue_id, subscriber_id): - issue_subscriber = IssueSubscriber.objects.get( - project=project_id, - subscriber=subscriber_id, - workspace__slug=slug, - issue=issue_id, - ) - issue_subscriber.delete() - return Response( - status=status.HTTP_204_NO_CONTENT, - ) - - def subscribe(self, request, slug, project_id, issue_id): - if IssueSubscriber.objects.filter( - issue_id=issue_id, - subscriber=request.user, - workspace__slug=slug, - project=project_id, - ).exists(): - return Response( - {"message": "User already subscribed to the issue."}, - status=status.HTTP_400_BAD_REQUEST, - ) - - subscriber = IssueSubscriber.objects.create( - issue_id=issue_id, - subscriber_id=request.user.id, - project_id=project_id, - ) - serializer = IssueSubscriberSerializer(subscriber) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - def unsubscribe(self, request, slug, project_id, issue_id): - issue_subscriber = IssueSubscriber.objects.get( - project=project_id, - subscriber=request.user, - workspace__slug=slug, - issue=issue_id, - ) - issue_subscriber.delete() - return Response( - status=status.HTTP_204_NO_CONTENT, - ) - - def subscription_status(self, request, slug, project_id, issue_id): - issue_subscriber = IssueSubscriber.objects.filter( - issue=issue_id, - subscriber=request.user, - workspace__slug=slug, - project=project_id, - ).exists() - return Response( - {"subscribed": issue_subscriber}, status=status.HTTP_200_OK - ) - - -class IssueReactionViewSet(BaseViewSet): - serializer_class = IssueReactionSerializer - model = IssueReaction - permission_classes = [ - ProjectLitePermission, - ] - - def get_queryset(self): - return ( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter(issue_id=self.kwargs.get("issue_id")) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - .order_by("-created_at") - .distinct() - ) - - def create(self, request, slug, project_id, issue_id): - serializer = IssueReactionSerializer(data=request.data) - if serializer.is_valid(): - serializer.save( - issue_id=issue_id, - project_id=project_id, - actor=request.user, - ) - issue_activity.delay( - type="issue_reaction.activity.created", - requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, slug, project_id, issue_id, reaction_code): - issue_reaction = IssueReaction.objects.get( - workspace__slug=slug, - project_id=project_id, - issue_id=issue_id, - reaction=reaction_code, - actor=request.user, - ) - issue_activity.delay( - type="issue_reaction.activity.deleted", - requested_data=None, - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("issue_id", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - { - "reaction": str(reaction_code), - "identifier": str(issue_reaction.id), - } - ), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - issue_reaction.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class CommentReactionViewSet(BaseViewSet): - serializer_class = CommentReactionSerializer - model = CommentReaction - permission_classes = [ - ProjectLitePermission, - ] - - def get_queryset(self): - return ( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter(comment_id=self.kwargs.get("comment_id")) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - .order_by("-created_at") - .distinct() - ) - - def create(self, request, slug, project_id, comment_id): - serializer = CommentReactionSerializer(data=request.data) - if serializer.is_valid(): - serializer.save( - project_id=project_id, - actor_id=request.user.id, - comment_id=comment_id, - ) - issue_activity.delay( - type="comment_reaction.activity.created", - requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), - actor_id=str(request.user.id), - issue_id=None, - project_id=str(project_id), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, slug, project_id, comment_id, reaction_code): - comment_reaction = CommentReaction.objects.get( - workspace__slug=slug, - project_id=project_id, - comment_id=comment_id, - reaction=reaction_code, - actor=request.user, - ) - issue_activity.delay( - type="comment_reaction.activity.deleted", - requested_data=None, - actor_id=str(self.request.user.id), - issue_id=None, - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - { - "reaction": str(reaction_code), - "identifier": str(comment_reaction.id), - "comment_id": str(comment_id), - } - ), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - comment_reaction.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class IssueRelationViewSet(BaseViewSet): - serializer_class = IssueRelationSerializer - model = IssueRelation - permission_classes = [ - ProjectEntityPermission, - ] - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter(issue_id=self.kwargs.get("issue_id")) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - .select_related("project") - .select_related("workspace") - .select_related("issue") - .distinct() - ) - - def list(self, request, slug, project_id, issue_id): - issue_relations = ( - IssueRelation.objects.filter( - Q(issue_id=issue_id) | Q(related_issue=issue_id) - ) - .filter(workspace__slug=self.kwargs.get("slug")) - .select_related("project") - .select_related("workspace") - .select_related("issue") - .order_by("-created_at") - .distinct() - ) - - blocking_issues = issue_relations.filter( - relation_type="blocked_by", related_issue_id=issue_id - ) - blocked_by_issues = issue_relations.filter( - relation_type="blocked_by", issue_id=issue_id - ) - duplicate_issues = issue_relations.filter( - issue_id=issue_id, relation_type="duplicate" - ) - duplicate_issues_related = issue_relations.filter( - related_issue_id=issue_id, relation_type="duplicate" - ) - relates_to_issues = issue_relations.filter( - issue_id=issue_id, relation_type="relates_to" - ) - relates_to_issues_related = issue_relations.filter( - related_issue_id=issue_id, relation_type="relates_to" - ) - - blocked_by_issues_serialized = IssueRelationSerializer( - blocked_by_issues, many=True - ).data - duplicate_issues_serialized = IssueRelationSerializer( - duplicate_issues, many=True - ).data - relates_to_issues_serialized = IssueRelationSerializer( - relates_to_issues, many=True - ).data - - # revere relation for blocked by issues - blocking_issues_serialized = RelatedIssueSerializer( - blocking_issues, many=True - ).data - # reverse relation for duplicate issues - duplicate_issues_related_serialized = RelatedIssueSerializer( - duplicate_issues_related, many=True - ).data - # reverse relation for related issues - relates_to_issues_related_serialized = RelatedIssueSerializer( - relates_to_issues_related, many=True - ).data - - response_data = { - "blocking": blocking_issues_serialized, - "blocked_by": blocked_by_issues_serialized, - "duplicate": duplicate_issues_serialized - + duplicate_issues_related_serialized, - "relates_to": relates_to_issues_serialized - + relates_to_issues_related_serialized, - } - - return Response(response_data, status=status.HTTP_200_OK) - - def create(self, request, slug, project_id, issue_id): - relation_type = request.data.get("relation_type", None) - issues = request.data.get("issues", []) - project = Project.objects.get(pk=project_id) - - issue_relation = IssueRelation.objects.bulk_create( - [ - IssueRelation( - issue_id=( - issue if relation_type == "blocking" else issue_id - ), - related_issue_id=( - issue_id if relation_type == "blocking" else issue - ), - relation_type=( - "blocked_by" - if relation_type == "blocking" - else relation_type - ), - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - updated_by=request.user, - ) - for issue in issues - ], - batch_size=10, - ignore_conflicts=True, - ) - - issue_activity.delay( - type="issue_relation.activity.created", - requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - - if relation_type == "blocking": - return Response( - RelatedIssueSerializer(issue_relation, many=True).data, - status=status.HTTP_201_CREATED, - ) - else: - return Response( - IssueRelationSerializer(issue_relation, many=True).data, - status=status.HTTP_201_CREATED, - ) - - def remove_relation(self, request, slug, project_id, issue_id): - relation_type = request.data.get("relation_type", None) - related_issue = request.data.get("related_issue", None) - - if relation_type == "blocking": - issue_relation = IssueRelation.objects.get( - workspace__slug=slug, - project_id=project_id, - issue_id=related_issue, - related_issue_id=issue_id, - ) - else: - issue_relation = IssueRelation.objects.get( - workspace__slug=slug, - project_id=project_id, - issue_id=issue_id, - related_issue_id=related_issue, - ) - current_instance = json.dumps( - IssueRelationSerializer(issue_relation).data, - cls=DjangoJSONEncoder, - ) - issue_relation.delete() - issue_activity.delay( - type="issue_relation.activity.deleted", - requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=current_instance, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(status=status.HTTP_204_NO_CONTENT) - - -class IssueDraftViewSet(BaseViewSet): - permission_classes = [ - ProjectEntityPermission, - ] - serializer_class = IssueFlatSerializer - model = Issue - - def get_queryset(self): - return ( - Issue.objects.filter(project_id=self.kwargs.get("project_id")) - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(is_draft=True) - .select_related("workspace", "project", "state", "parent") - .prefetch_related("assignees", "labels", "issue_module__module") - .annotate(cycle_id=F("issue_cycle__cycle_id")) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=~Q(labels__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=~Q(assignees__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "issue_module__module_id", - distinct=True, - filter=~Q(issue_module__module_id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) - ).distinct() - - @method_decorator(gzip_page) - def list(self, request, slug, project_id): - filters = issue_filters(request.query_params, "GET") - # Custom ordering for priority and state - priority_order = ["urgent", "high", "medium", "low", "none"] - state_order = [ - "backlog", - "unstarted", - "started", - "completed", - "cancelled", - ] - - order_by_param = request.GET.get("order_by", "-created_at") - - issue_queryset = self.get_queryset().filter(**filters) - - # Priority Ordering - if order_by_param == "priority" or order_by_param == "-priority": - priority_order = ( - priority_order - if order_by_param == "priority" - else priority_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - priority_order=Case( - *[ - When(priority=p, then=Value(i)) - for i, p in enumerate(priority_order) - ], - output_field=CharField(), - ) - ).order_by("priority_order") - - # State Ordering - elif order_by_param in [ - "state__name", - "state__group", - "-state__name", - "-state__group", - ]: - state_order = ( - state_order - if order_by_param in ["state__name", "state__group"] - else state_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - state_order=Case( - *[ - When(state__group=state_group, then=Value(i)) - for i, state_group in enumerate(state_order) - ], - default=Value(len(state_order)), - output_field=CharField(), - ) - ).order_by("state_order") - # assignee and label ordering - elif order_by_param in [ - "labels__name", - "-labels__name", - "assignees__first_name", - "-assignees__first_name", - ]: - issue_queryset = issue_queryset.annotate( - max_values=Max( - order_by_param[1::] - if order_by_param.startswith("-") - else order_by_param - ) - ).order_by( - "-max_values" - if order_by_param.startswith("-") - else "max_values" - ) - else: - issue_queryset = issue_queryset.order_by(order_by_param) - - # Only use serializer when expand else return by values - if self.expand or self.fields: - issues = IssueSerializer( - issue_queryset, - many=True, - fields=self.fields, - expand=self.expand, - ).data - else: - issues = issue_queryset.values( - "id", - "name", - "state_id", - "sort_order", - "completed_at", - "estimate_point", - "priority", - "start_date", - "target_date", - "sequence_id", - "project_id", - "parent_id", - "cycle_id", - "module_ids", - "label_ids", - "assignee_ids", - "sub_issues_count", - "created_at", - "updated_at", - "created_by", - "updated_by", - "attachment_count", - "link_count", - "is_draft", - "archived_at", - ) - return Response(issues, status=status.HTTP_200_OK) - - def create(self, request, slug, project_id): - project = Project.objects.get(pk=project_id) - - serializer = IssueCreateSerializer( - data=request.data, - context={ - "project_id": project_id, - "workspace_id": project.workspace_id, - "default_assignee_id": project.default_assignee_id, - }, - ) - - if serializer.is_valid(): - serializer.save(is_draft=True) - - # Track the issue - issue_activity.delay( - type="issue_draft.activity.created", - requested_data=json.dumps( - self.request.data, cls=DjangoJSONEncoder - ), - actor_id=str(request.user.id), - issue_id=str(serializer.data.get("id", None)), - project_id=str(project_id), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - issue = ( - self.get_queryset().filter(pk=serializer.data["id"]).first() - ) - return Response( - IssueSerializer(issue).data, status=status.HTTP_201_CREATED - ) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def partial_update(self, request, slug, project_id, pk): - issue = self.get_queryset().filter(pk=pk).first() - - if not issue: - return Response( - {"error": "Issue does not exist"}, - status=status.HTTP_404_NOT_FOUND, - ) - - serializer = IssueCreateSerializer( - issue, data=request.data, partial=True - ) - - if serializer.is_valid(): - serializer.save() - issue_activity.delay( - type="issue_draft.activity.updated", - requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("pk", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - IssueSerializer(issue).data, - cls=DjangoJSONEncoder, - ), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(status=status.HTTP_204_NO_CONTENT) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def retrieve(self, request, slug, project_id, pk=None): - issue = ( - self.get_queryset() - .filter(pk=pk) - .prefetch_related( - Prefetch( - "issue_reactions", - queryset=IssueReaction.objects.select_related( - "issue", "actor" - ), - ) - ) - .prefetch_related( - Prefetch( - "issue_attachment", - queryset=IssueAttachment.objects.select_related("issue"), - ) - ) - .prefetch_related( - Prefetch( - "issue_link", - queryset=IssueLink.objects.select_related("created_by"), - ) - ) - .annotate( - is_subscribed=Exists( - IssueSubscriber.objects.filter( - workspace__slug=slug, - project_id=project_id, - issue_id=OuterRef("pk"), - subscriber=request.user, - ) - ) - ) - ).first() - - if not issue: - return Response( - {"error": "The required object does not exist."}, - status=status.HTTP_404_NOT_FOUND, - ) - serializer = IssueDetailSerializer(issue, expand=self.expand) - return Response(serializer.data, status=status.HTTP_200_OK) - - def destroy(self, request, slug, project_id, pk=None): - issue = Issue.objects.get( - workspace__slug=slug, project_id=project_id, pk=pk - ) - issue.delete() - issue_activity.delay( - type="issue_draft.activity.deleted", - requested_data=json.dumps({"issue_id": str(pk)}), - actor_id=str(request.user.id), - issue_id=str(pk), - project_id=str(project_id), - current_instance={}, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/issue/activity.py b/apiserver/plane/app/views/issue/activity.py new file mode 100644 index 000000000..ea6e9b389 --- /dev/null +++ b/apiserver/plane/app/views/issue/activity.py @@ -0,0 +1,85 @@ +# Python imports +from itertools import chain + +# Django imports +from django.db.models import ( + Prefetch, + Q, +) +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseAPIView +from plane.app.serializers import ( + IssueActivitySerializer, + IssueCommentSerializer, +) +from plane.app.permissions import ProjectEntityPermission +from plane.db.models import ( + IssueActivity, + IssueComment, + CommentReaction, +) + + +class IssueActivityEndpoint(BaseAPIView): + permission_classes = [ + ProjectEntityPermission, + ] + + @method_decorator(gzip_page) + def get(self, request, slug, project_id, issue_id): + filters = {} + if request.GET.get("created_at__gt", None) is not None: + filters = {"created_at__gt": request.GET.get("created_at__gt")} + + issue_activities = ( + IssueActivity.objects.filter(issue_id=issue_id) + .filter( + ~Q(field__in=["comment", "vote", "reaction", "draft"]), + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + workspace__slug=slug, + ) + .filter(**filters) + .select_related("actor", "workspace", "issue", "project") + ).order_by("created_at") + issue_comments = ( + IssueComment.objects.filter(issue_id=issue_id) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + workspace__slug=slug, + ) + .filter(**filters) + .order_by("created_at") + .select_related("actor", "issue", "project", "workspace") + .prefetch_related( + Prefetch( + "comment_reactions", + queryset=CommentReaction.objects.select_related("actor"), + ) + ) + ) + issue_activities = IssueActivitySerializer( + issue_activities, many=True + ).data + issue_comments = IssueCommentSerializer(issue_comments, many=True).data + + if request.GET.get("activity_type", None) == "issue-property": + return Response(issue_activities, status=status.HTTP_200_OK) + + if request.GET.get("activity_type", None) == "issue-comment": + return Response(issue_comments, status=status.HTTP_200_OK) + + result_list = sorted( + chain(issue_activities, issue_comments), + key=lambda instance: instance["created_at"], + ) + + return Response(result_list, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/issue/archive.py b/apiserver/plane/app/views/issue/archive.py new file mode 100644 index 000000000..540715a24 --- /dev/null +++ b/apiserver/plane/app/views/issue/archive.py @@ -0,0 +1,347 @@ +# Python imports +import json + +# Django imports +from django.utils import timezone +from django.db.models import ( + Prefetch, + OuterRef, + Func, + F, + Q, + Case, + Value, + CharField, + When, + Exists, + Max, + UUIDField, +) +from django.core.serializers.json import DjangoJSONEncoder +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.db.models.functions import Coalesce + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet +from plane.app.serializers import ( + IssueSerializer, + IssueFlatSerializer, + IssueDetailSerializer, +) +from plane.app.permissions import ( + ProjectEntityPermission, +) +from plane.db.models import ( + Issue, + IssueLink, + IssueAttachment, + IssueSubscriber, + IssueReaction, +) +from plane.bgtasks.issue_activites_task import issue_activity +from plane.utils.issue_filters import issue_filters + + +class IssueArchiveViewSet(BaseViewSet): + permission_classes = [ + ProjectEntityPermission, + ] + serializer_class = IssueFlatSerializer + model = Issue + + def get_queryset(self): + return ( + Issue.objects.annotate( + sub_issues_count=Issue.objects.filter(parent=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .filter(archived_at__isnull=False) + .filter(project_id=self.kwargs.get("project_id")) + .filter(workspace__slug=self.kwargs.get("slug")) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels", "issue_module__module") + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + ) + + @method_decorator(gzip_page) + def list(self, request, slug, project_id): + filters = issue_filters(request.query_params, "GET") + show_sub_issues = request.GET.get("show_sub_issues", "true") + + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", "none"] + state_order = [ + "backlog", + "unstarted", + "started", + "completed", + "cancelled", + ] + + order_by_param = request.GET.get("order_by", "-created_at") + + issue_queryset = self.get_queryset().filter(**filters) + + # Priority Ordering + if order_by_param == "priority" or order_by_param == "-priority": + priority_order = ( + priority_order + if order_by_param == "priority" + else priority_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), + ) + ).order_by("priority_order") + + # State Ordering + elif order_by_param in [ + "state__name", + "state__group", + "-state__name", + "-state__group", + ]: + state_order = ( + state_order + if order_by_param in ["state__name", "state__group"] + else state_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + state_order=Case( + *[ + When(state__group=state_group, then=Value(i)) + for i, state_group in enumerate(state_order) + ], + default=Value(len(state_order)), + output_field=CharField(), + ) + ).order_by("state_order") + # assignee and label ordering + elif order_by_param in [ + "labels__name", + "-labels__name", + "assignees__first_name", + "-assignees__first_name", + ]: + issue_queryset = issue_queryset.annotate( + max_values=Max( + order_by_param[1::] + if order_by_param.startswith("-") + else order_by_param + ) + ).order_by( + "-max_values" + if order_by_param.startswith("-") + else "max_values" + ) + else: + issue_queryset = issue_queryset.order_by(order_by_param) + + issue_queryset = ( + issue_queryset + if show_sub_issues == "true" + else issue_queryset.filter(parent__isnull=True) + ) + if self.expand or self.fields: + issues = IssueSerializer( + issue_queryset, + many=True, + fields=self.fields, + ).data + else: + issues = issue_queryset.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + return Response(issues, status=status.HTTP_200_OK) + + def retrieve(self, request, slug, project_id, pk=None): + issue = ( + self.get_queryset() + .filter(pk=pk) + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related( + "issue", "actor" + ), + ) + ) + .prefetch_related( + Prefetch( + "issue_attachment", + queryset=IssueAttachment.objects.select_related("issue"), + ) + ) + .prefetch_related( + Prefetch( + "issue_link", + queryset=IssueLink.objects.select_related("created_by"), + ) + ) + .annotate( + is_subscribed=Exists( + IssueSubscriber.objects.filter( + workspace__slug=slug, + project_id=project_id, + issue_id=OuterRef("pk"), + subscriber=request.user, + ) + ) + ) + ).first() + if not issue: + return Response( + {"error": "The required object does not exist."}, + status=status.HTTP_404_NOT_FOUND, + ) + serializer = IssueDetailSerializer(issue, expand=self.expand) + return Response(serializer.data, status=status.HTTP_200_OK) + + def archive(self, request, slug, project_id, pk=None): + issue = Issue.issue_objects.get( + workspace__slug=slug, + project_id=project_id, + pk=pk, + ) + if issue.state.group not in ["completed", "cancelled"]: + return Response( + { + "error": "Can only archive completed or cancelled state group issue" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + issue_activity.delay( + type="issue.activity.updated", + requested_data=json.dumps( + { + "archived_at": str(timezone.now().date()), + "automation": False, + } + ), + actor_id=str(request.user.id), + issue_id=str(issue.id), + project_id=str(project_id), + current_instance=json.dumps( + IssueSerializer(issue).data, cls=DjangoJSONEncoder + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + issue.archived_at = timezone.now().date() + issue.save() + + return Response( + {"archived_at": str(issue.archived_at)}, status=status.HTTP_200_OK + ) + + def unarchive(self, request, slug, project_id, pk=None): + issue = Issue.objects.get( + workspace__slug=slug, + project_id=project_id, + archived_at__isnull=False, + pk=pk, + ) + issue_activity.delay( + type="issue.activity.updated", + requested_data=json.dumps({"archived_at": None}), + actor_id=str(request.user.id), + issue_id=str(issue.id), + project_id=str(project_id), + current_instance=json.dumps( + IssueSerializer(issue).data, cls=DjangoJSONEncoder + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + issue.archived_at = None + issue.save() + + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/issue/attachment.py b/apiserver/plane/app/views/issue/attachment.py new file mode 100644 index 000000000..c2b8ad6ff --- /dev/null +++ b/apiserver/plane/app/views/issue/attachment.py @@ -0,0 +1,73 @@ +# Python imports +import json + +# Django imports +from django.utils import timezone +from django.core.serializers.json import DjangoJSONEncoder + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status +from rest_framework.parsers import MultiPartParser, FormParser + +# Module imports +from .. import BaseAPIView +from plane.app.serializers import IssueAttachmentSerializer +from plane.app.permissions import ProjectEntityPermission +from plane.db.models import IssueAttachment +from plane.bgtasks.issue_activites_task import issue_activity + + +class IssueAttachmentEndpoint(BaseAPIView): + serializer_class = IssueAttachmentSerializer + permission_classes = [ + ProjectEntityPermission, + ] + model = IssueAttachment + parser_classes = (MultiPartParser, FormParser) + + def post(self, request, slug, project_id, issue_id): + serializer = IssueAttachmentSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(project_id=project_id, issue_id=issue_id) + issue_activity.delay( + type="attachment.activity.created", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + serializer.data, + cls=DjangoJSONEncoder, + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def delete(self, request, slug, project_id, issue_id, pk): + issue_attachment = IssueAttachment.objects.get(pk=pk) + issue_attachment.asset.delete(save=False) + issue_attachment.delete() + issue_activity.delay( + type="attachment.activity.deleted", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + + return Response(status=status.HTTP_204_NO_CONTENT) + + def get(self, request, slug, project_id, issue_id): + issue_attachments = IssueAttachment.objects.filter( + issue_id=issue_id, workspace__slug=slug, project_id=project_id + ) + serializer = IssueAttachmentSerializer(issue_attachments, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py new file mode 100644 index 000000000..63d4358b0 --- /dev/null +++ b/apiserver/plane/app/views/issue/base.py @@ -0,0 +1,686 @@ +# Python imports +import json +import random +from itertools import chain + +# Django imports +from django.utils import timezone +from django.db.models import ( + Prefetch, + OuterRef, + Func, + F, + Q, + Case, + Value, + CharField, + When, + Exists, + Max, +) +from django.core.serializers.json import DjangoJSONEncoder +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page +from django.db import IntegrityError +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.db.models import Value, UUIDField +from django.db.models.functions import Coalesce + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status +from rest_framework.parsers import MultiPartParser, FormParser + +# Module imports +from .. import BaseViewSet, BaseAPIView, WebhookMixin +from plane.app.serializers import ( + IssueActivitySerializer, + IssueCommentSerializer, + IssuePropertySerializer, + IssueSerializer, + IssueCreateSerializer, + LabelSerializer, + IssueFlatSerializer, + IssueLinkSerializer, + IssueLiteSerializer, + IssueAttachmentSerializer, + IssueSubscriberSerializer, + ProjectMemberLiteSerializer, + IssueReactionSerializer, + CommentReactionSerializer, + IssueRelationSerializer, + RelatedIssueSerializer, + IssueDetailSerializer, +) +from plane.app.permissions import ( + ProjectEntityPermission, + WorkSpaceAdminPermission, + ProjectMemberPermission, + ProjectLitePermission, +) +from plane.db.models import ( + Project, + Issue, + IssueActivity, + IssueComment, + IssueProperty, + Label, + IssueLink, + IssueAttachment, + IssueSubscriber, + ProjectMember, + IssueReaction, + CommentReaction, + IssueRelation, +) +from plane.bgtasks.issue_activites_task import issue_activity +from plane.utils.grouper import group_results +from plane.utils.issue_filters import issue_filters +from collections import defaultdict +from plane.utils.cache import invalidate_cache + +class IssueListEndpoint(BaseAPIView): + + permission_classes = [ + ProjectEntityPermission, + ] + + def get(self, request, slug, project_id): + issue_ids = request.GET.get("issues", False) + + if not issue_ids: + return Response( + {"error": "Issues are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + issue_ids = [ + issue_id for issue_id in issue_ids.split(",") if issue_id != "" + ] + + queryset = ( + Issue.issue_objects.filter( + workspace__slug=slug, project_id=project_id, pk__in=issue_ids + ) + .filter(workspace__slug=self.kwargs.get("slug")) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels", "issue_module__module") + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + ).distinct() + + filters = issue_filters(request.query_params, "GET") + + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", "none"] + state_order = [ + "backlog", + "unstarted", + "started", + "completed", + "cancelled", + ] + + order_by_param = request.GET.get("order_by", "-created_at") + + issue_queryset = queryset.filter(**filters) + + # Priority Ordering + if order_by_param == "priority" or order_by_param == "-priority": + priority_order = ( + priority_order + if order_by_param == "priority" + else priority_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), + ) + ).order_by("priority_order") + + # State Ordering + elif order_by_param in [ + "state__name", + "state__group", + "-state__name", + "-state__group", + ]: + state_order = ( + state_order + if order_by_param in ["state__name", "state__group"] + else state_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + state_order=Case( + *[ + When(state__group=state_group, then=Value(i)) + for i, state_group in enumerate(state_order) + ], + default=Value(len(state_order)), + output_field=CharField(), + ) + ).order_by("state_order") + # assignee and label ordering + elif order_by_param in [ + "labels__name", + "-labels__name", + "assignees__first_name", + "-assignees__first_name", + ]: + issue_queryset = issue_queryset.annotate( + max_values=Max( + order_by_param[1::] + if order_by_param.startswith("-") + else order_by_param + ) + ).order_by( + "-max_values" + if order_by_param.startswith("-") + else "max_values" + ) + else: + issue_queryset = issue_queryset.order_by(order_by_param) + + if self.fields or self.expand: + issues = IssueSerializer( + queryset, many=True, fields=self.fields, expand=self.expand + ).data + else: + issues = issue_queryset.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + return Response(issues, status=status.HTTP_200_OK) + + +class IssueViewSet(WebhookMixin, BaseViewSet): + def get_serializer_class(self): + return ( + IssueCreateSerializer + if self.action in ["create", "update", "partial_update"] + else IssueSerializer + ) + + model = Issue + webhook_event = "issue" + permission_classes = [ + ProjectEntityPermission, + ] + + search_fields = [ + "name", + ] + + filterset_fields = [ + "state__name", + "assignees__id", + "workspace__id", + ] + + def get_queryset(self): + return ( + Issue.issue_objects.filter( + project_id=self.kwargs.get("project_id") + ) + .filter(workspace__slug=self.kwargs.get("slug")) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels", "issue_module__module") + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + ).distinct() + + @method_decorator(gzip_page) + def list(self, request, slug, project_id): + filters = issue_filters(request.query_params, "GET") + order_by_param = request.GET.get("order_by", "-created_at") + + issue_queryset = self.get_queryset().filter(**filters) + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", "none"] + state_order = [ + "backlog", + "unstarted", + "started", + "completed", + "cancelled", + ] + + # Priority Ordering + if order_by_param == "priority" or order_by_param == "-priority": + priority_order = ( + priority_order + if order_by_param == "priority" + else priority_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), + ) + ).order_by("priority_order") + + # State Ordering + elif order_by_param in [ + "state__name", + "state__group", + "-state__name", + "-state__group", + ]: + state_order = ( + state_order + if order_by_param in ["state__name", "state__group"] + else state_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + state_order=Case( + *[ + When(state__group=state_group, then=Value(i)) + for i, state_group in enumerate(state_order) + ], + default=Value(len(state_order)), + output_field=CharField(), + ) + ).order_by("state_order") + # assignee and label ordering + elif order_by_param in [ + "labels__name", + "-labels__name", + "assignees__first_name", + "-assignees__first_name", + ]: + issue_queryset = issue_queryset.annotate( + max_values=Max( + order_by_param[1::] + if order_by_param.startswith("-") + else order_by_param + ) + ).order_by( + "-max_values" + if order_by_param.startswith("-") + else "max_values" + ) + else: + issue_queryset = issue_queryset.order_by(order_by_param) + + # Only use serializer when expand or fields else return by values + if self.expand or self.fields: + issues = IssueSerializer( + issue_queryset, + many=True, + fields=self.fields, + expand=self.expand, + ).data + else: + issues = issue_queryset.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + return Response(issues, status=status.HTTP_200_OK) + + def create(self, request, slug, project_id): + project = Project.objects.get(pk=project_id) + + serializer = IssueCreateSerializer( + data=request.data, + context={ + "project_id": project_id, + "workspace_id": project.workspace_id, + "default_assignee_id": project.default_assignee_id, + }, + ) + + if serializer.is_valid(): + serializer.save() + + # Track the issue + issue_activity.delay( + type="issue.activity.created", + requested_data=json.dumps( + self.request.data, cls=DjangoJSONEncoder + ), + actor_id=str(request.user.id), + issue_id=str(serializer.data.get("id", None)), + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + issue = ( + self.get_queryset() + .filter(pk=serializer.data["id"]) + .values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + .first() + ) + return Response(issue, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def retrieve(self, request, slug, project_id, pk=None): + issue = ( + self.get_queryset() + .filter(pk=pk) + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related( + "issue", "actor" + ), + ) + ) + .prefetch_related( + Prefetch( + "issue_attachment", + queryset=IssueAttachment.objects.select_related("issue"), + ) + ) + .prefetch_related( + Prefetch( + "issue_link", + queryset=IssueLink.objects.select_related("created_by"), + ) + ) + .annotate( + is_subscribed=Exists( + IssueSubscriber.objects.filter( + workspace__slug=slug, + project_id=project_id, + issue_id=OuterRef("pk"), + subscriber=request.user, + ) + ) + ) + ).first() + if not issue: + return Response( + {"error": "The required object does not exist."}, + status=status.HTTP_404_NOT_FOUND, + ) + + serializer = IssueDetailSerializer(issue, expand=self.expand) + return Response(serializer.data, status=status.HTTP_200_OK) + + def partial_update(self, request, slug, project_id, pk=None): + issue = self.get_queryset().filter(pk=pk).first() + + if not issue: + return Response( + {"error": "Issue not found"}, + status=status.HTTP_404_NOT_FOUND, + ) + + current_instance = json.dumps( + IssueSerializer(issue).data, cls=DjangoJSONEncoder + ) + + requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) + serializer = IssueCreateSerializer( + issue, data=request.data, partial=True + ) + if serializer.is_valid(): + serializer.save() + issue_activity.delay( + type="issue.activity.updated", + requested_data=requested_data, + actor_id=str(request.user.id), + issue_id=str(pk), + project_id=str(project_id), + current_instance=current_instance, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + issue = self.get_queryset().filter(pk=pk).first() + return Response(status=status.HTTP_204_NO_CONTENT) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, pk=None): + issue = Issue.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) + issue.delete() + issue_activity.delay( + type="issue.activity.deleted", + requested_data=json.dumps({"issue_id": str(pk)}), + actor_id=str(request.user.id), + issue_id=str(pk), + project_id=str(project_id), + current_instance={}, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(status=status.HTTP_204_NO_CONTENT) + + +class IssueUserDisplayPropertyEndpoint(BaseAPIView): + permission_classes = [ + ProjectLitePermission, + ] + + def patch(self, request, slug, project_id): + issue_property = IssueProperty.objects.get( + user=request.user, + project_id=project_id, + ) + + issue_property.filters = request.data.get( + "filters", issue_property.filters + ) + issue_property.display_filters = request.data.get( + "display_filters", issue_property.display_filters + ) + issue_property.display_properties = request.data.get( + "display_properties", issue_property.display_properties + ) + issue_property.save() + serializer = IssuePropertySerializer(issue_property) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def get(self, request, slug, project_id): + issue_property, _ = IssueProperty.objects.get_or_create( + user=request.user, project_id=project_id + ) + serializer = IssuePropertySerializer(issue_property) + return Response(serializer.data, status=status.HTTP_200_OK) + + +class BulkDeleteIssuesEndpoint(BaseAPIView): + permission_classes = [ + ProjectEntityPermission, + ] + + def delete(self, request, slug, project_id): + issue_ids = request.data.get("issue_ids", []) + + if not len(issue_ids): + return Response( + {"error": "Issue IDs are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + issues = Issue.issue_objects.filter( + workspace__slug=slug, project_id=project_id, pk__in=issue_ids + ) + + total_issues = len(issues) + + issues.delete() + + return Response( + {"message": f"{total_issues} issues were deleted"}, + status=status.HTTP_200_OK, + ) diff --git a/apiserver/plane/app/views/issue/comment.py b/apiserver/plane/app/views/issue/comment.py new file mode 100644 index 000000000..eb2d5834c --- /dev/null +++ b/apiserver/plane/app/views/issue/comment.py @@ -0,0 +1,219 @@ +# Python imports +import json + +# Django imports +from django.utils import timezone +from django.db.models import Exists +from django.core.serializers.json import DjangoJSONEncoder + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet, WebhookMixin +from plane.app.serializers import ( + IssueCommentSerializer, + CommentReactionSerializer, +) +from plane.app.permissions import ProjectLitePermission +from plane.db.models import ( + IssueComment, + ProjectMember, + CommentReaction, +) +from plane.bgtasks.issue_activites_task import issue_activity + + +class IssueCommentViewSet(WebhookMixin, BaseViewSet): + serializer_class = IssueCommentSerializer + model = IssueComment + webhook_event = "issue_comment" + permission_classes = [ + ProjectLitePermission, + ] + + filterset_fields = [ + "issue__id", + "workspace__id", + ] + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(issue_id=self.kwargs.get("issue_id")) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) + .select_related("project") + .select_related("workspace") + .select_related("issue") + .annotate( + is_member=Exists( + ProjectMember.objects.filter( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), + member_id=self.request.user.id, + is_active=True, + ) + ) + ) + .distinct() + ) + + def create(self, request, slug, project_id, issue_id): + serializer = IssueCommentSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( + project_id=project_id, + issue_id=issue_id, + actor=request.user, + ) + issue_activity.delay( + type="comment.activity.created", + requested_data=json.dumps( + serializer.data, cls=DjangoJSONEncoder + ), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id")), + project_id=str(self.kwargs.get("project_id")), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def partial_update(self, request, slug, project_id, issue_id, pk): + issue_comment = IssueComment.objects.get( + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + pk=pk, + ) + requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) + current_instance = json.dumps( + IssueCommentSerializer(issue_comment).data, + cls=DjangoJSONEncoder, + ) + serializer = IssueCommentSerializer( + issue_comment, data=request.data, partial=True + ) + if serializer.is_valid(): + serializer.save() + issue_activity.delay( + type="comment.activity.updated", + requested_data=requested_data, + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=current_instance, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, issue_id, pk): + issue_comment = IssueComment.objects.get( + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + pk=pk, + ) + current_instance = json.dumps( + IssueCommentSerializer(issue_comment).data, + cls=DjangoJSONEncoder, + ) + issue_comment.delete() + issue_activity.delay( + type="comment.activity.deleted", + requested_data=json.dumps({"comment_id": str(pk)}), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=current_instance, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(status=status.HTTP_204_NO_CONTENT) + + +class CommentReactionViewSet(BaseViewSet): + serializer_class = CommentReactionSerializer + model = CommentReaction + permission_classes = [ + ProjectLitePermission, + ] + + def get_queryset(self): + return ( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(comment_id=self.kwargs.get("comment_id")) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) + .order_by("-created_at") + .distinct() + ) + + def create(self, request, slug, project_id, comment_id): + serializer = CommentReactionSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( + project_id=project_id, + actor_id=request.user.id, + comment_id=comment_id, + ) + issue_activity.delay( + type="comment_reaction.activity.created", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=None, + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, comment_id, reaction_code): + comment_reaction = CommentReaction.objects.get( + workspace__slug=slug, + project_id=project_id, + comment_id=comment_id, + reaction=reaction_code, + actor=request.user, + ) + issue_activity.delay( + type="comment_reaction.activity.deleted", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=None, + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "reaction": str(reaction_code), + "identifier": str(comment_reaction.id), + "comment_id": str(comment_id), + } + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + comment_reaction.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/issue/draft.py b/apiserver/plane/app/views/issue/draft.py new file mode 100644 index 000000000..08032934b --- /dev/null +++ b/apiserver/plane/app/views/issue/draft.py @@ -0,0 +1,367 @@ +# Python imports +import json + +# Django imports +from django.utils import timezone +from django.db.models import ( + Prefetch, + OuterRef, + Func, + F, + Q, + Case, + Value, + CharField, + When, + Exists, + Max, + UUIDField, +) +from django.core.serializers.json import DjangoJSONEncoder +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.db.models.functions import Coalesce + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet +from plane.app.serializers import ( + IssueSerializer, + IssueCreateSerializer, + IssueFlatSerializer, + IssueDetailSerializer, +) +from plane.app.permissions import ProjectEntityPermission +from plane.db.models import ( + Project, + Issue, + IssueLink, + IssueAttachment, + IssueSubscriber, + IssueReaction, +) +from plane.bgtasks.issue_activites_task import issue_activity +from plane.utils.issue_filters import issue_filters + + +class IssueDraftViewSet(BaseViewSet): + permission_classes = [ + ProjectEntityPermission, + ] + serializer_class = IssueFlatSerializer + model = Issue + + def get_queryset(self): + return ( + Issue.objects.filter(project_id=self.kwargs.get("project_id")) + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(is_draft=True) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels", "issue_module__module") + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + ).distinct() + + @method_decorator(gzip_page) + def list(self, request, slug, project_id): + filters = issue_filters(request.query_params, "GET") + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] + + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", "none"] + state_order = [ + "backlog", + "unstarted", + "started", + "completed", + "cancelled", + ] + + order_by_param = request.GET.get("order_by", "-created_at") + + issue_queryset = self.get_queryset().filter(**filters) + + # Priority Ordering + if order_by_param == "priority" or order_by_param == "-priority": + priority_order = ( + priority_order + if order_by_param == "priority" + else priority_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), + ) + ).order_by("priority_order") + + # State Ordering + elif order_by_param in [ + "state__name", + "state__group", + "-state__name", + "-state__group", + ]: + state_order = ( + state_order + if order_by_param in ["state__name", "state__group"] + else state_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + state_order=Case( + *[ + When(state__group=state_group, then=Value(i)) + for i, state_group in enumerate(state_order) + ], + default=Value(len(state_order)), + output_field=CharField(), + ) + ).order_by("state_order") + # assignee and label ordering + elif order_by_param in [ + "labels__name", + "-labels__name", + "assignees__first_name", + "-assignees__first_name", + ]: + issue_queryset = issue_queryset.annotate( + max_values=Max( + order_by_param[1::] + if order_by_param.startswith("-") + else order_by_param + ) + ).order_by( + "-max_values" + if order_by_param.startswith("-") + else "max_values" + ) + else: + issue_queryset = issue_queryset.order_by(order_by_param) + + # Only use serializer when expand else return by values + if self.expand or self.fields: + issues = IssueSerializer( + issue_queryset, + many=True, + fields=self.fields, + expand=self.expand, + ).data + else: + issues = issue_queryset.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + return Response(issues, status=status.HTTP_200_OK) + + def create(self, request, slug, project_id): + project = Project.objects.get(pk=project_id) + + serializer = IssueCreateSerializer( + data=request.data, + context={ + "project_id": project_id, + "workspace_id": project.workspace_id, + "default_assignee_id": project.default_assignee_id, + }, + ) + + if serializer.is_valid(): + serializer.save(is_draft=True) + + # Track the issue + issue_activity.delay( + type="issue_draft.activity.created", + requested_data=json.dumps( + self.request.data, cls=DjangoJSONEncoder + ), + actor_id=str(request.user.id), + issue_id=str(serializer.data.get("id", None)), + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + issue = ( + self.get_queryset().filter(pk=serializer.data["id"]).first() + ) + return Response( + IssueSerializer(issue).data, status=status.HTTP_201_CREATED + ) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def partial_update(self, request, slug, project_id, pk): + issue = self.get_queryset().filter(pk=pk).first() + + if not issue: + return Response( + {"error": "Issue does not exist"}, + status=status.HTTP_404_NOT_FOUND, + ) + + serializer = IssueCreateSerializer( + issue, data=request.data, partial=True + ) + + if serializer.is_valid(): + serializer.save() + issue_activity.delay( + type="issue_draft.activity.updated", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("pk", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + IssueSerializer(issue).data, + cls=DjangoJSONEncoder, + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(status=status.HTTP_204_NO_CONTENT) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def retrieve(self, request, slug, project_id, pk=None): + issue = ( + self.get_queryset() + .filter(pk=pk) + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related( + "issue", "actor" + ), + ) + ) + .prefetch_related( + Prefetch( + "issue_attachment", + queryset=IssueAttachment.objects.select_related("issue"), + ) + ) + .prefetch_related( + Prefetch( + "issue_link", + queryset=IssueLink.objects.select_related("created_by"), + ) + ) + .annotate( + is_subscribed=Exists( + IssueSubscriber.objects.filter( + workspace__slug=slug, + project_id=project_id, + issue_id=OuterRef("pk"), + subscriber=request.user, + ) + ) + ) + ).first() + + if not issue: + return Response( + {"error": "The required object does not exist."}, + status=status.HTTP_404_NOT_FOUND, + ) + serializer = IssueDetailSerializer(issue, expand=self.expand) + return Response(serializer.data, status=status.HTTP_200_OK) + + def destroy(self, request, slug, project_id, pk=None): + issue = Issue.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) + issue.delete() + issue_activity.delay( + type="issue_draft.activity.deleted", + requested_data=json.dumps({"issue_id": str(pk)}), + actor_id=str(request.user.id), + issue_id=str(pk), + project_id=str(project_id), + current_instance={}, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/issue/label.py b/apiserver/plane/app/views/issue/label.py new file mode 100644 index 000000000..557c2018f --- /dev/null +++ b/apiserver/plane/app/views/issue/label.py @@ -0,0 +1,105 @@ +# Python imports +import random + +# Django imports +from django.db import IntegrityError + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet, BaseAPIView +from plane.app.serializers import LabelSerializer +from plane.app.permissions import ( + ProjectMemberPermission, +) +from plane.db.models import ( + Project, + Label, +) +from plane.utils.cache import invalidate_cache + + +class LabelViewSet(BaseViewSet): + serializer_class = LabelSerializer + model = Label + permission_classes = [ + ProjectMemberPermission, + ] + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(project__project_projectmember__member=self.request.user) + .select_related("project") + .select_related("workspace") + .select_related("parent") + .distinct() + .order_by("sort_order") + ) + + @invalidate_cache( + path="/api/workspaces/:slug/labels/", url_params=True, user=False + ) + def create(self, request, slug, project_id): + try: + serializer = LabelSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(project_id=project_id) + return Response( + serializer.data, status=status.HTTP_201_CREATED + ) + return Response( + serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) + except IntegrityError: + return Response( + { + "error": "Label with the same name already exists in the project" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + @invalidate_cache( + path="/api/workspaces/:slug/labels/", url_params=True, user=False + ) + def partial_update(self, request, *args, **kwargs): + return super().partial_update(request, *args, **kwargs) + + @invalidate_cache( + path="/api/workspaces/:slug/labels/", url_params=True, user=False + ) + def destroy(self, request, *args, **kwargs): + return super().destroy(request, *args, **kwargs) + + +class BulkCreateIssueLabelsEndpoint(BaseAPIView): + def post(self, request, slug, project_id): + label_data = request.data.get("label_data", []) + project = Project.objects.get(pk=project_id) + + labels = Label.objects.bulk_create( + [ + Label( + name=label.get("name", "Migrated"), + description=label.get("description", "Migrated Issue"), + color="#" + "%06x" % random.randint(0, 0xFFFFFF), + project_id=project_id, + workspace_id=project.workspace_id, + created_by=request.user, + updated_by=request.user, + ) + for label in label_data + ], + batch_size=50, + ignore_conflicts=True, + ) + + return Response( + {"labels": LabelSerializer(labels, many=True).data}, + status=status.HTTP_201_CREATED, + ) diff --git a/apiserver/plane/app/views/issue/link.py b/apiserver/plane/app/views/issue/link.py new file mode 100644 index 000000000..ca3290759 --- /dev/null +++ b/apiserver/plane/app/views/issue/link.py @@ -0,0 +1,120 @@ +# Python imports +import json + +# Django imports +from django.utils import timezone +from django.core.serializers.json import DjangoJSONEncoder + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet +from plane.app.serializers import IssueLinkSerializer +from plane.app.permissions import ProjectEntityPermission +from plane.db.models import IssueLink +from plane.bgtasks.issue_activites_task import issue_activity + + +class IssueLinkViewSet(BaseViewSet): + permission_classes = [ + ProjectEntityPermission, + ] + + model = IssueLink + serializer_class = IssueLinkSerializer + + def get_queryset(self): + return ( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(issue_id=self.kwargs.get("issue_id")) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) + .order_by("-created_at") + .distinct() + ) + + def create(self, request, slug, project_id, issue_id): + serializer = IssueLinkSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( + project_id=project_id, + issue_id=issue_id, + ) + issue_activity.delay( + type="link.activity.created", + requested_data=json.dumps( + serializer.data, cls=DjangoJSONEncoder + ), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id")), + project_id=str(self.kwargs.get("project_id")), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def partial_update(self, request, slug, project_id, issue_id, pk): + issue_link = IssueLink.objects.get( + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + pk=pk, + ) + requested_data = json.dumps(request.data, cls=DjangoJSONEncoder) + current_instance = json.dumps( + IssueLinkSerializer(issue_link).data, + cls=DjangoJSONEncoder, + ) + serializer = IssueLinkSerializer( + issue_link, data=request.data, partial=True + ) + if serializer.is_valid(): + serializer.save() + issue_activity.delay( + type="link.activity.updated", + requested_data=requested_data, + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=current_instance, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, issue_id, pk): + issue_link = IssueLink.objects.get( + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + pk=pk, + ) + current_instance = json.dumps( + IssueLinkSerializer(issue_link).data, + cls=DjangoJSONEncoder, + ) + issue_activity.delay( + type="link.activity.deleted", + requested_data=json.dumps({"link_id": str(pk)}), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=current_instance, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + issue_link.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/issue/reaction.py b/apiserver/plane/app/views/issue/reaction.py new file mode 100644 index 000000000..c6f6823be --- /dev/null +++ b/apiserver/plane/app/views/issue/reaction.py @@ -0,0 +1,89 @@ +# Python imports +import json + +# Django imports +from django.utils import timezone +from django.core.serializers.json import DjangoJSONEncoder + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet +from plane.app.serializers import IssueReactionSerializer +from plane.app.permissions import ProjectLitePermission +from plane.db.models import IssueReaction +from plane.bgtasks.issue_activites_task import issue_activity + + +class IssueReactionViewSet(BaseViewSet): + serializer_class = IssueReactionSerializer + model = IssueReaction + permission_classes = [ + ProjectLitePermission, + ] + + def get_queryset(self): + return ( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(issue_id=self.kwargs.get("issue_id")) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) + .order_by("-created_at") + .distinct() + ) + + def create(self, request, slug, project_id, issue_id): + serializer = IssueReactionSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( + issue_id=issue_id, + project_id=project_id, + actor=request.user, + ) + issue_activity.delay( + type="issue_reaction.activity.created", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, issue_id, reaction_code): + issue_reaction = IssueReaction.objects.get( + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + reaction=reaction_code, + actor=request.user, + ) + issue_activity.delay( + type="issue_reaction.activity.deleted", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "reaction": str(reaction_code), + "identifier": str(issue_reaction.id), + } + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + issue_reaction.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/issue/relation.py b/apiserver/plane/app/views/issue/relation.py new file mode 100644 index 000000000..45a5dc9a7 --- /dev/null +++ b/apiserver/plane/app/views/issue/relation.py @@ -0,0 +1,204 @@ +# Python imports +import json + +# Django imports +from django.utils import timezone +from django.db.models import Q +from django.core.serializers.json import DjangoJSONEncoder + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet +from plane.app.serializers import ( + IssueRelationSerializer, + RelatedIssueSerializer, +) +from plane.app.permissions import ProjectEntityPermission +from plane.db.models import ( + Project, + IssueRelation, +) +from plane.bgtasks.issue_activites_task import issue_activity + + +class IssueRelationViewSet(BaseViewSet): + serializer_class = IssueRelationSerializer + model = IssueRelation + permission_classes = [ + ProjectEntityPermission, + ] + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(issue_id=self.kwargs.get("issue_id")) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) + .select_related("project") + .select_related("workspace") + .select_related("issue") + .distinct() + ) + + def list(self, request, slug, project_id, issue_id): + issue_relations = ( + IssueRelation.objects.filter( + Q(issue_id=issue_id) | Q(related_issue=issue_id) + ) + .filter(workspace__slug=self.kwargs.get("slug")) + .select_related("project") + .select_related("workspace") + .select_related("issue") + .order_by("-created_at") + .distinct() + ) + + blocking_issues = issue_relations.filter( + relation_type="blocked_by", related_issue_id=issue_id + ) + blocked_by_issues = issue_relations.filter( + relation_type="blocked_by", issue_id=issue_id + ) + duplicate_issues = issue_relations.filter( + issue_id=issue_id, relation_type="duplicate" + ) + duplicate_issues_related = issue_relations.filter( + related_issue_id=issue_id, relation_type="duplicate" + ) + relates_to_issues = issue_relations.filter( + issue_id=issue_id, relation_type="relates_to" + ) + relates_to_issues_related = issue_relations.filter( + related_issue_id=issue_id, relation_type="relates_to" + ) + + blocked_by_issues_serialized = IssueRelationSerializer( + blocked_by_issues, many=True + ).data + duplicate_issues_serialized = IssueRelationSerializer( + duplicate_issues, many=True + ).data + relates_to_issues_serialized = IssueRelationSerializer( + relates_to_issues, many=True + ).data + + # revere relation for blocked by issues + blocking_issues_serialized = RelatedIssueSerializer( + blocking_issues, many=True + ).data + # reverse relation for duplicate issues + duplicate_issues_related_serialized = RelatedIssueSerializer( + duplicate_issues_related, many=True + ).data + # reverse relation for related issues + relates_to_issues_related_serialized = RelatedIssueSerializer( + relates_to_issues_related, many=True + ).data + + response_data = { + "blocking": blocking_issues_serialized, + "blocked_by": blocked_by_issues_serialized, + "duplicate": duplicate_issues_serialized + + duplicate_issues_related_serialized, + "relates_to": relates_to_issues_serialized + + relates_to_issues_related_serialized, + } + + return Response(response_data, status=status.HTTP_200_OK) + + def create(self, request, slug, project_id, issue_id): + relation_type = request.data.get("relation_type", None) + issues = request.data.get("issues", []) + project = Project.objects.get(pk=project_id) + + issue_relation = IssueRelation.objects.bulk_create( + [ + IssueRelation( + issue_id=( + issue if relation_type == "blocking" else issue_id + ), + related_issue_id=( + issue_id if relation_type == "blocking" else issue + ), + relation_type=( + "blocked_by" + if relation_type == "blocking" + else relation_type + ), + project_id=project_id, + workspace_id=project.workspace_id, + created_by=request.user, + updated_by=request.user, + ) + for issue in issues + ], + batch_size=10, + ignore_conflicts=True, + ) + + issue_activity.delay( + type="issue_relation.activity.created", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + + if relation_type == "blocking": + return Response( + RelatedIssueSerializer(issue_relation, many=True).data, + status=status.HTTP_201_CREATED, + ) + else: + return Response( + IssueRelationSerializer(issue_relation, many=True).data, + status=status.HTTP_201_CREATED, + ) + + def remove_relation(self, request, slug, project_id, issue_id): + relation_type = request.data.get("relation_type", None) + related_issue = request.data.get("related_issue", None) + + if relation_type == "blocking": + issue_relation = IssueRelation.objects.get( + workspace__slug=slug, + project_id=project_id, + issue_id=related_issue, + related_issue_id=issue_id, + ) + else: + issue_relation = IssueRelation.objects.get( + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + related_issue_id=related_issue, + ) + current_instance = json.dumps( + IssueRelationSerializer(issue_relation).data, + cls=DjangoJSONEncoder, + ) + issue_relation.delete() + issue_activity.delay( + type="issue_relation.activity.deleted", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=current_instance, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/issue/sub_issue.py b/apiserver/plane/app/views/issue/sub_issue.py new file mode 100644 index 000000000..6ec4a2de1 --- /dev/null +++ b/apiserver/plane/app/views/issue/sub_issue.py @@ -0,0 +1,195 @@ +# Python imports +import json + +# Django imports +from django.utils import timezone +from django.db.models import ( + OuterRef, + Func, + F, + Q, + Value, + UUIDField, +) +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.db.models.functions import Coalesce + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseAPIView +from plane.app.serializers import IssueSerializer +from plane.app.permissions import ProjectEntityPermission +from plane.db.models import ( + Issue, + IssueLink, + IssueAttachment, +) +from plane.bgtasks.issue_activites_task import issue_activity +from collections import defaultdict + + +class SubIssuesEndpoint(BaseAPIView): + permission_classes = [ + ProjectEntityPermission, + ] + + @method_decorator(gzip_page) + def get(self, request, slug, project_id, issue_id): + sub_issues = ( + Issue.issue_objects.filter( + parent_id=issue_id, workspace__slug=slug + ) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels", "issue_module__module") + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + .annotate(state_group=F("state__group")) + ) + + # create's a dict with state group name with their respective issue id's + result = defaultdict(list) + for sub_issue in sub_issues: + result[sub_issue.state_group].append(str(sub_issue.id)) + + sub_issues = sub_issues.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + return Response( + { + "sub_issues": sub_issues, + "state_distribution": result, + }, + status=status.HTTP_200_OK, + ) + + # Assign multiple sub issues + def post(self, request, slug, project_id, issue_id): + parent_issue = Issue.issue_objects.get(pk=issue_id) + sub_issue_ids = request.data.get("sub_issue_ids", []) + + if not len(sub_issue_ids): + return Response( + {"error": "Sub Issue IDs are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids) + + for sub_issue in sub_issues: + sub_issue.parent = parent_issue + + _ = Issue.objects.bulk_update(sub_issues, ["parent"], batch_size=10) + + updated_sub_issues = Issue.issue_objects.filter( + id__in=sub_issue_ids + ).annotate(state_group=F("state__group")) + + # Track the issue + _ = [ + issue_activity.delay( + type="issue.activity.updated", + requested_data=json.dumps({"parent": str(issue_id)}), + actor_id=str(request.user.id), + issue_id=str(sub_issue_id), + project_id=str(project_id), + current_instance=json.dumps({"parent": str(sub_issue_id)}), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + for sub_issue_id in sub_issue_ids + ] + + # create's a dict with state group name with their respective issue id's + result = defaultdict(list) + for sub_issue in updated_sub_issues: + result[sub_issue.state_group].append(str(sub_issue.id)) + + serializer = IssueSerializer( + updated_sub_issues, + many=True, + ) + return Response( + { + "sub_issues": serializer.data, + "state_distribution": result, + }, + status=status.HTTP_200_OK, + ) diff --git a/apiserver/plane/app/views/issue/subscriber.py b/apiserver/plane/app/views/issue/subscriber.py new file mode 100644 index 000000000..61e09e4a2 --- /dev/null +++ b/apiserver/plane/app/views/issue/subscriber.py @@ -0,0 +1,124 @@ +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet +from plane.app.serializers import ( + IssueSubscriberSerializer, + ProjectMemberLiteSerializer, +) +from plane.app.permissions import ( + ProjectEntityPermission, + ProjectLitePermission, +) +from plane.db.models import ( + IssueSubscriber, + ProjectMember, +) + + +class IssueSubscriberViewSet(BaseViewSet): + serializer_class = IssueSubscriberSerializer + model = IssueSubscriber + + permission_classes = [ + ProjectEntityPermission, + ] + + def get_permissions(self): + if self.action in ["subscribe", "unsubscribe", "subscription_status"]: + self.permission_classes = [ + ProjectLitePermission, + ] + else: + self.permission_classes = [ + ProjectEntityPermission, + ] + + return super(IssueSubscriberViewSet, self).get_permissions() + + def perform_create(self, serializer): + serializer.save( + project_id=self.kwargs.get("project_id"), + issue_id=self.kwargs.get("issue_id"), + ) + + def get_queryset(self): + return ( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(issue_id=self.kwargs.get("issue_id")) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) + .order_by("-created_at") + .distinct() + ) + + def list(self, request, slug, project_id, issue_id): + members = ProjectMember.objects.filter( + workspace__slug=slug, + project_id=project_id, + is_active=True, + ).select_related("member") + serializer = ProjectMemberLiteSerializer(members, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + def destroy(self, request, slug, project_id, issue_id, subscriber_id): + issue_subscriber = IssueSubscriber.objects.get( + project=project_id, + subscriber=subscriber_id, + workspace__slug=slug, + issue=issue_id, + ) + issue_subscriber.delete() + return Response( + status=status.HTTP_204_NO_CONTENT, + ) + + def subscribe(self, request, slug, project_id, issue_id): + if IssueSubscriber.objects.filter( + issue_id=issue_id, + subscriber=request.user, + workspace__slug=slug, + project=project_id, + ).exists(): + return Response( + {"message": "User already subscribed to the issue."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + subscriber = IssueSubscriber.objects.create( + issue_id=issue_id, + subscriber_id=request.user.id, + project_id=project_id, + ) + serializer = IssueSubscriberSerializer(subscriber) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def unsubscribe(self, request, slug, project_id, issue_id): + issue_subscriber = IssueSubscriber.objects.get( + project=project_id, + subscriber=request.user, + workspace__slug=slug, + issue=issue_id, + ) + issue_subscriber.delete() + return Response( + status=status.HTTP_204_NO_CONTENT, + ) + + def subscription_status(self, request, slug, project_id, issue_id): + issue_subscriber = IssueSubscriber.objects.filter( + issue=issue_id, + subscriber=request.user, + workspace__slug=slug, + project=project_id, + ).exists() + return Response( + {"subscribed": issue_subscriber}, status=status.HTTP_200_OK + ) diff --git a/apiserver/plane/app/views/module.py b/apiserver/plane/app/views/module/base.py similarity index 67% rename from apiserver/plane/app/views/module.py rename to apiserver/plane/app/views/module/base.py index c93e0b01c..cd87442d2 100644 --- a/apiserver/plane/app/views/module.py +++ b/apiserver/plane/app/views/module/base.py @@ -3,9 +3,7 @@ import json # Django Imports from django.utils import timezone -from django.db.models import Prefetch, F, OuterRef, Func, Exists, Count, Q -from django.utils.decorators import method_decorator -from django.views.decorators.gzip import gzip_page +from django.db.models import Prefetch, F, OuterRef, Exists, Count, Q from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.fields import ArrayField from django.db.models import Value, UUIDField @@ -16,14 +14,12 @@ from rest_framework.response import Response from rest_framework import status # Module imports -from . import BaseViewSet, BaseAPIView, WebhookMixin +from .. import BaseViewSet, BaseAPIView, WebhookMixin from plane.app.serializers import ( ModuleWriteSerializer, ModuleSerializer, - ModuleIssueSerializer, ModuleLinkSerializer, ModuleFavoriteSerializer, - IssueSerializer, ModuleUserPropertiesSerializer, ModuleDetailSerializer, ) @@ -38,12 +34,9 @@ from plane.db.models import ( Issue, ModuleLink, ModuleFavorite, - IssueLink, - IssueAttachment, ModuleUserProperties, ) from plane.bgtasks.issue_activites_task import issue_activity -from plane.utils.issue_filters import issue_filters from plane.utils.analytics_plot import burndown_plot @@ -426,232 +419,6 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): return Response(status=status.HTTP_204_NO_CONTENT) -class ModuleIssueViewSet(WebhookMixin, BaseViewSet): - serializer_class = ModuleIssueSerializer - model = ModuleIssue - webhook_event = "module_issue" - bulk = True - - filterset_fields = [ - "issue__labels__id", - "issue__assignees__id", - ] - - permission_classes = [ - ProjectEntityPermission, - ] - - def get_queryset(self): - return ( - Issue.issue_objects.filter( - project_id=self.kwargs.get("project_id"), - workspace__slug=self.kwargs.get("slug"), - issue_module__module_id=self.kwargs.get("module_id"), - ) - .select_related("workspace", "project", "state", "parent") - .prefetch_related("assignees", "labels", "issue_module__module") - .annotate(cycle_id=F("issue_cycle__cycle_id")) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=~Q(labels__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=~Q(assignees__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "issue_module__module_id", - distinct=True, - filter=~Q(issue_module__module_id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) - ).distinct() - - @method_decorator(gzip_page) - def list(self, request, slug, project_id, module_id): - fields = [ - field - for field in request.GET.get("fields", "").split(",") - if field - ] - filters = issue_filters(request.query_params, "GET") - issue_queryset = self.get_queryset().filter(**filters) - if self.fields or self.expand: - issues = IssueSerializer( - issue_queryset, many=True, fields=fields if fields else None - ).data - else: - issues = issue_queryset.values( - "id", - "name", - "state_id", - "sort_order", - "completed_at", - "estimate_point", - "priority", - "start_date", - "target_date", - "sequence_id", - "project_id", - "parent_id", - "cycle_id", - "module_ids", - "label_ids", - "assignee_ids", - "sub_issues_count", - "created_at", - "updated_at", - "created_by", - "updated_by", - "attachment_count", - "link_count", - "is_draft", - "archived_at", - ) - return Response(issues, status=status.HTTP_200_OK) - - # create multiple issues inside a module - def create_module_issues(self, request, slug, project_id, module_id): - issues = request.data.get("issues", []) - if not issues: - return Response( - {"error": "Issues are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - project = Project.objects.get(pk=project_id) - _ = ModuleIssue.objects.bulk_create( - [ - ModuleIssue( - issue_id=str(issue), - module_id=module_id, - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - updated_by=request.user, - ) - for issue in issues - ], - batch_size=10, - ignore_conflicts=True, - ) - # Bulk Update the activity - _ = [ - issue_activity.delay( - type="module.activity.created", - requested_data=json.dumps({"module_id": str(module_id)}), - actor_id=str(request.user.id), - issue_id=str(issue), - project_id=project_id, - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - for issue in issues - ] - return Response({"message": "success"}, status=status.HTTP_201_CREATED) - - # create multiple module inside an issue - def create_issue_modules(self, request, slug, project_id, issue_id): - modules = request.data.get("modules", []) - if not modules: - return Response( - {"error": "Modules are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - project = Project.objects.get(pk=project_id) - _ = ModuleIssue.objects.bulk_create( - [ - ModuleIssue( - issue_id=issue_id, - module_id=module, - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - updated_by=request.user, - ) - for module in modules - ], - batch_size=10, - ignore_conflicts=True, - ) - # Bulk Update the activity - _ = [ - issue_activity.delay( - type="module.activity.created", - requested_data=json.dumps({"module_id": module}), - actor_id=str(request.user.id), - issue_id=issue_id, - project_id=project_id, - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - for module in modules - ] - - return Response({"message": "success"}, status=status.HTTP_201_CREATED) - - def destroy(self, request, slug, project_id, module_id, issue_id): - module_issue = ModuleIssue.objects.get( - workspace__slug=slug, - project_id=project_id, - module_id=module_id, - issue_id=issue_id, - ) - issue_activity.delay( - type="module.activity.deleted", - requested_data=json.dumps({"module_id": str(module_id)}), - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=json.dumps( - {"module_name": module_issue.module.name} - ), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - module_issue.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - class ModuleLinkViewSet(BaseViewSet): permission_classes = [ ProjectEntityPermission, diff --git a/apiserver/plane/app/views/module/issue.py b/apiserver/plane/app/views/module/issue.py new file mode 100644 index 000000000..cfa8ee478 --- /dev/null +++ b/apiserver/plane/app/views/module/issue.py @@ -0,0 +1,259 @@ +# Python imports +import json + +# Django Imports +from django.utils import timezone +from django.db.models import F, OuterRef, Func, Q +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.db.models import Value, UUIDField +from django.db.models.functions import Coalesce + +# Third party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet, WebhookMixin +from plane.app.serializers import ( + ModuleIssueSerializer, + IssueSerializer, +) +from plane.app.permissions import ProjectEntityPermission +from plane.db.models import ( + ModuleIssue, + Project, + Issue, + IssueLink, + IssueAttachment, +) +from plane.bgtasks.issue_activites_task import issue_activity +from plane.utils.issue_filters import issue_filters + + +class ModuleIssueViewSet(WebhookMixin, BaseViewSet): + serializer_class = ModuleIssueSerializer + model = ModuleIssue + webhook_event = "module_issue" + bulk = True + + filterset_fields = [ + "issue__labels__id", + "issue__assignees__id", + ] + + permission_classes = [ + ProjectEntityPermission, + ] + + def get_queryset(self): + return ( + Issue.issue_objects.filter( + project_id=self.kwargs.get("project_id"), + workspace__slug=self.kwargs.get("slug"), + issue_module__module_id=self.kwargs.get("module_id"), + ) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels", "issue_module__module") + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + ).distinct() + + @method_decorator(gzip_page) + def list(self, request, slug, project_id, module_id): + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] + filters = issue_filters(request.query_params, "GET") + issue_queryset = self.get_queryset().filter(**filters) + if self.fields or self.expand: + issues = IssueSerializer( + issue_queryset, many=True, fields=fields if fields else None + ).data + else: + issues = issue_queryset.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + return Response(issues, status=status.HTTP_200_OK) + + # create multiple issues inside a module + def create_module_issues(self, request, slug, project_id, module_id): + issues = request.data.get("issues", []) + if not issues: + return Response( + {"error": "Issues are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + project = Project.objects.get(pk=project_id) + _ = ModuleIssue.objects.bulk_create( + [ + ModuleIssue( + issue_id=str(issue), + module_id=module_id, + project_id=project_id, + workspace_id=project.workspace_id, + created_by=request.user, + updated_by=request.user, + ) + for issue in issues + ], + batch_size=10, + ignore_conflicts=True, + ) + # Bulk Update the activity + _ = [ + issue_activity.delay( + type="module.activity.created", + requested_data=json.dumps({"module_id": str(module_id)}), + actor_id=str(request.user.id), + issue_id=str(issue), + project_id=project_id, + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + for issue in issues + ] + return Response({"message": "success"}, status=status.HTTP_201_CREATED) + + # create multiple module inside an issue + def create_issue_modules(self, request, slug, project_id, issue_id): + modules = request.data.get("modules", []) + if not modules: + return Response( + {"error": "Modules are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + project = Project.objects.get(pk=project_id) + _ = ModuleIssue.objects.bulk_create( + [ + ModuleIssue( + issue_id=issue_id, + module_id=module, + project_id=project_id, + workspace_id=project.workspace_id, + created_by=request.user, + updated_by=request.user, + ) + for module in modules + ], + batch_size=10, + ignore_conflicts=True, + ) + # Bulk Update the activity + _ = [ + issue_activity.delay( + type="module.activity.created", + requested_data=json.dumps({"module_id": module}), + actor_id=str(request.user.id), + issue_id=issue_id, + project_id=project_id, + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + for module in modules + ] + + return Response({"message": "success"}, status=status.HTTP_201_CREATED) + + def destroy(self, request, slug, project_id, module_id, issue_id): + module_issue = ModuleIssue.objects.get( + workspace__slug=slug, + project_id=project_id, + module_id=module_id, + issue_id=issue_id, + ) + issue_activity.delay( + type="module.activity.deleted", + requested_data=json.dumps({"module_id": str(module_id)}), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=json.dumps( + {"module_name": module_issue.module.name} + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + module_issue.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/notification.py b/apiserver/plane/app/views/notification/base.py similarity index 99% rename from apiserver/plane/app/views/notification.py rename to apiserver/plane/app/views/notification/base.py index a6f84f65a..8dae618db 100644 --- a/apiserver/plane/app/views/notification.py +++ b/apiserver/plane/app/views/notification/base.py @@ -8,7 +8,7 @@ from rest_framework.response import Response from plane.utils.paginator import BasePaginator # Module imports -from .base import BaseViewSet, BaseAPIView +from ..base import BaseViewSet, BaseAPIView from plane.db.models import ( Notification, IssueAssignee, diff --git a/apiserver/plane/app/views/page.py b/apiserver/plane/app/views/page/base.py similarity index 99% rename from apiserver/plane/app/views/page.py rename to apiserver/plane/app/views/page/base.py index 21d461fe1..34a9ee638 100644 --- a/apiserver/plane/app/views/page.py +++ b/apiserver/plane/app/views/page/base.py @@ -26,7 +26,7 @@ from plane.db.models import ( ) # Module imports -from .base import BaseAPIView, BaseViewSet +from ..base import BaseAPIView, BaseViewSet def unarchive_archive_page_and_descendants(page_id, archived_at): diff --git a/apiserver/plane/app/views/project.py b/apiserver/plane/app/views/project.py deleted file mode 100644 index d1f0159af..000000000 --- a/apiserver/plane/app/views/project.py +++ /dev/null @@ -1,1146 +0,0 @@ -# Python imports -from datetime import datetime - -import boto3 -import jwt -from django.conf import settings - -# Django imports -from django.core.exceptions import ValidationError -from django.core.validators import validate_email -from django.db import IntegrityError -from django.db.models import ( - Exists, - F, - Func, - OuterRef, - Prefetch, - Q, - Subquery, -) -from django.utils import timezone - -# Third Party imports -from rest_framework import serializers, status -from rest_framework.permissions import AllowAny -from rest_framework.response import Response - -# Module imports -from plane.app.permissions import ( - ProjectBasePermission, - ProjectLitePermission, - ProjectMemberPermission, - WorkspaceUserPermission, -) -from plane.app.serializers import ( - ProjectDeployBoardSerializer, - ProjectFavoriteSerializer, - ProjectListSerializer, - ProjectMemberAdminSerializer, - ProjectMemberInviteSerializer, - ProjectMemberRoleSerializer, - ProjectMemberSerializer, - ProjectSerializer, -) -from plane.bgtasks.project_invitation_task import project_invitation -from plane.db.models import ( - Cycle, - Inbox, - IssueProperty, - Module, - Project, - ProjectDeployBoard, - ProjectFavorite, - ProjectIdentifier, - ProjectMember, - ProjectMemberInvite, - State, - TeamMember, - User, - Workspace, - WorkspaceMember, -) -from plane.utils.cache import cache_response - -from .base import BaseAPIView, BaseViewSet, WebhookMixin - - -class ProjectViewSet(WebhookMixin, BaseViewSet): - serializer_class = ProjectListSerializer - model = Project - webhook_event = "project" - - permission_classes = [ - ProjectBasePermission, - ] - - def get_queryset(self): - sort_order = ProjectMember.objects.filter( - member=self.request.user, - project_id=OuterRef("pk"), - workspace__slug=self.kwargs.get("slug"), - is_active=True, - ).values("sort_order") - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter( - Q(project_projectmember__member=self.request.user) - | Q(network=2) - ) - .select_related( - "workspace", - "workspace__owner", - "default_assignee", - "project_lead", - ) - .annotate( - is_favorite=Exists( - ProjectFavorite.objects.filter( - user=self.request.user, - project_id=OuterRef("pk"), - workspace__slug=self.kwargs.get("slug"), - ) - ) - ) - .annotate( - is_member=Exists( - ProjectMember.objects.filter( - member=self.request.user, - project_id=OuterRef("pk"), - workspace__slug=self.kwargs.get("slug"), - is_active=True, - ) - ) - ) - .annotate( - total_members=ProjectMember.objects.filter( - project_id=OuterRef("id"), - member__is_bot=False, - is_active=True, - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - total_cycles=Cycle.objects.filter(project_id=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - total_modules=Module.objects.filter(project_id=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - member_role=ProjectMember.objects.filter( - project_id=OuterRef("pk"), - member_id=self.request.user.id, - is_active=True, - ).values("role") - ) - .annotate( - is_deployed=Exists( - ProjectDeployBoard.objects.filter( - project_id=OuterRef("pk"), - workspace__slug=self.kwargs.get("slug"), - ) - ) - ) - .annotate(sort_order=Subquery(sort_order)) - .prefetch_related( - Prefetch( - "project_projectmember", - queryset=ProjectMember.objects.filter( - workspace__slug=self.kwargs.get("slug"), - is_active=True, - ).select_related("member"), - to_attr="members_list", - ) - ) - .distinct() - ) - - def list(self, request, slug): - fields = [ - field - for field in request.GET.get("fields", "").split(",") - if field - ] - projects = self.get_queryset().order_by("sort_order", "name") - if request.GET.get("per_page", False) and request.GET.get( - "cursor", False - ): - return self.paginate( - request=request, - queryset=(projects), - on_results=lambda projects: ProjectListSerializer( - projects, many=True - ).data, - ) - projects = ProjectListSerializer( - projects, many=True, fields=fields if fields else None - ).data - return Response(projects, status=status.HTTP_200_OK) - - def create(self, request, slug): - try: - workspace = Workspace.objects.get(slug=slug) - - serializer = ProjectSerializer( - data={**request.data}, context={"workspace_id": workspace.id} - ) - if serializer.is_valid(): - serializer.save() - - # Add the user as Administrator to the project - _ = ProjectMember.objects.create( - project_id=serializer.data["id"], - member=request.user, - role=20, - ) - # Also create the issue property for the user - _ = IssueProperty.objects.create( - project_id=serializer.data["id"], - user=request.user, - ) - - if serializer.data["project_lead"] is not None and str( - serializer.data["project_lead"] - ) != str(request.user.id): - ProjectMember.objects.create( - project_id=serializer.data["id"], - member_id=serializer.data["project_lead"], - role=20, - ) - # Also create the issue property for the user - IssueProperty.objects.create( - project_id=serializer.data["id"], - user_id=serializer.data["project_lead"], - ) - - # Default states - states = [ - { - "name": "Backlog", - "color": "#A3A3A3", - "sequence": 15000, - "group": "backlog", - "default": True, - }, - { - "name": "Todo", - "color": "#3A3A3A", - "sequence": 25000, - "group": "unstarted", - }, - { - "name": "In Progress", - "color": "#F59E0B", - "sequence": 35000, - "group": "started", - }, - { - "name": "Done", - "color": "#16A34A", - "sequence": 45000, - "group": "completed", - }, - { - "name": "Cancelled", - "color": "#EF4444", - "sequence": 55000, - "group": "cancelled", - }, - ] - - State.objects.bulk_create( - [ - State( - name=state["name"], - color=state["color"], - project=serializer.instance, - sequence=state["sequence"], - workspace=serializer.instance.workspace, - group=state["group"], - default=state.get("default", False), - created_by=request.user, - ) - for state in states - ] - ) - - project = ( - self.get_queryset() - .filter(pk=serializer.data["id"]) - .first() - ) - serializer = ProjectListSerializer(project) - return Response( - serializer.data, status=status.HTTP_201_CREATED - ) - return Response( - serializer.errors, - status=status.HTTP_400_BAD_REQUEST, - ) - except IntegrityError as e: - if "already exists" in str(e): - return Response( - {"name": "The project name is already taken"}, - status=status.HTTP_410_GONE, - ) - except Workspace.DoesNotExist: - return Response( - {"error": "Workspace does not exist"}, - status=status.HTTP_404_NOT_FOUND, - ) - except serializers.ValidationError: - return Response( - {"identifier": "The project identifier is already taken"}, - status=status.HTTP_410_GONE, - ) - - def partial_update(self, request, slug, pk=None): - try: - workspace = Workspace.objects.get(slug=slug) - - project = Project.objects.get(pk=pk) - - serializer = ProjectSerializer( - project, - data={**request.data}, - context={"workspace_id": workspace.id}, - partial=True, - ) - - if serializer.is_valid(): - serializer.save() - if serializer.data["inbox_view"]: - Inbox.objects.get_or_create( - name=f"{project.name} Inbox", - project=project, - is_default=True, - ) - - # Create the triage state in Backlog group - State.objects.get_or_create( - name="Triage", - group="backlog", - description="Default state for managing all Inbox Issues", - project_id=pk, - color="#ff7700", - ) - - project = ( - self.get_queryset() - .filter(pk=serializer.data["id"]) - .first() - ) - serializer = ProjectListSerializer(project) - return Response(serializer.data, status=status.HTTP_200_OK) - return Response( - serializer.errors, status=status.HTTP_400_BAD_REQUEST - ) - - except IntegrityError as e: - if "already exists" in str(e): - return Response( - {"name": "The project name is already taken"}, - status=status.HTTP_410_GONE, - ) - except (Project.DoesNotExist, Workspace.DoesNotExist): - return Response( - {"error": "Project does not exist"}, - status=status.HTTP_404_NOT_FOUND, - ) - except serializers.ValidationError: - return Response( - {"identifier": "The project identifier is already taken"}, - status=status.HTTP_410_GONE, - ) - - -class ProjectInvitationsViewset(BaseViewSet): - serializer_class = ProjectMemberInviteSerializer - model = ProjectMemberInvite - - search_fields = [] - - permission_classes = [ - ProjectBasePermission, - ] - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .select_related("project") - .select_related("workspace", "workspace__owner") - ) - - def create(self, request, slug, project_id): - emails = request.data.get("emails", []) - - # Check if email is provided - if not emails: - return Response( - {"error": "Emails are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - requesting_user = ProjectMember.objects.get( - workspace__slug=slug, - project_id=project_id, - member_id=request.user.id, - ) - - # Check if any invited user has an higher role - if len( - [ - email - for email in emails - if int(email.get("role", 10)) > requesting_user.role - ] - ): - return Response( - {"error": "You cannot invite a user with higher role"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - workspace = Workspace.objects.get(slug=slug) - - project_invitations = [] - for email in emails: - try: - validate_email(email.get("email")) - project_invitations.append( - ProjectMemberInvite( - email=email.get("email").strip().lower(), - project_id=project_id, - workspace_id=workspace.id, - token=jwt.encode( - { - "email": email, - "timestamp": datetime.now().timestamp(), - }, - settings.SECRET_KEY, - algorithm="HS256", - ), - role=email.get("role", 10), - created_by=request.user, - ) - ) - except ValidationError: - return Response( - { - "error": f"Invalid email - {email} provided a valid email address is required to send the invite" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Create workspace member invite - project_invitations = ProjectMemberInvite.objects.bulk_create( - project_invitations, batch_size=10, ignore_conflicts=True - ) - current_site = request.META.get("HTTP_ORIGIN") - - # Send invitations - for invitation in project_invitations: - project_invitations.delay( - invitation.email, - project_id, - invitation.token, - current_site, - request.user.email, - ) - - return Response( - { - "message": "Email sent successfully", - }, - status=status.HTTP_200_OK, - ) - - -class UserProjectInvitationsViewset(BaseViewSet): - serializer_class = ProjectMemberInviteSerializer - model = ProjectMemberInvite - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(email=self.request.user.email) - .select_related("workspace", "workspace__owner", "project") - ) - - def create(self, request, slug): - project_ids = request.data.get("project_ids", []) - - # Get the workspace user role - workspace_member = WorkspaceMember.objects.get( - member=request.user, - workspace__slug=slug, - is_active=True, - ) - - workspace_role = workspace_member.role - workspace = workspace_member.workspace - - # If the user was already part of workspace - _ = ProjectMember.objects.filter( - workspace__slug=slug, - project_id__in=project_ids, - member=request.user, - ).update(is_active=True) - - ProjectMember.objects.bulk_create( - [ - ProjectMember( - project_id=project_id, - member=request.user, - role=15 if workspace_role >= 15 else 10, - workspace=workspace, - created_by=request.user, - ) - for project_id in project_ids - ], - ignore_conflicts=True, - ) - - IssueProperty.objects.bulk_create( - [ - IssueProperty( - project_id=project_id, - user=request.user, - workspace=workspace, - created_by=request.user, - ) - for project_id in project_ids - ], - ignore_conflicts=True, - ) - - return Response( - {"message": "Projects joined successfully"}, - status=status.HTTP_201_CREATED, - ) - - -class ProjectJoinEndpoint(BaseAPIView): - permission_classes = [ - AllowAny, - ] - - def post(self, request, slug, project_id, pk): - project_invite = ProjectMemberInvite.objects.get( - pk=pk, - project_id=project_id, - workspace__slug=slug, - ) - - email = request.data.get("email", "") - - if email == "" or project_invite.email != email: - return Response( - {"error": "You do not have permission to join the project"}, - status=status.HTTP_403_FORBIDDEN, - ) - - if project_invite.responded_at is None: - project_invite.accepted = request.data.get("accepted", False) - project_invite.responded_at = timezone.now() - project_invite.save() - - if project_invite.accepted: - # Check if the user account exists - user = User.objects.filter(email=email).first() - - # Check if user is a part of workspace - workspace_member = WorkspaceMember.objects.filter( - workspace__slug=slug, member=user - ).first() - # Add him to workspace - if workspace_member is None: - _ = WorkspaceMember.objects.create( - workspace_id=project_invite.workspace_id, - member=user, - role=( - 15 - if project_invite.role >= 15 - else project_invite.role - ), - ) - else: - # Else make him active - workspace_member.is_active = True - workspace_member.save() - - # Check if the user was already a member of project then activate the user - project_member = ProjectMember.objects.filter( - workspace_id=project_invite.workspace_id, member=user - ).first() - if project_member is None: - # Create a Project Member - _ = ProjectMember.objects.create( - workspace_id=project_invite.workspace_id, - member=user, - role=project_invite.role, - ) - else: - project_member.is_active = True - project_member.role = project_member.role - project_member.save() - - return Response( - {"message": "Project Invitation Accepted"}, - status=status.HTTP_200_OK, - ) - - return Response( - {"message": "Project Invitation was not accepted"}, - status=status.HTTP_200_OK, - ) - - return Response( - {"error": "You have already responded to the invitation request"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - def get(self, request, slug, project_id, pk): - project_invitation = ProjectMemberInvite.objects.get( - workspace__slug=slug, project_id=project_id, pk=pk - ) - serializer = ProjectMemberInviteSerializer(project_invitation) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class ProjectMemberViewSet(BaseViewSet): - serializer_class = ProjectMemberAdminSerializer - model = ProjectMember - permission_classes = [ - ProjectMemberPermission, - ] - - def get_permissions(self): - if self.action == "leave": - self.permission_classes = [ - ProjectLitePermission, - ] - else: - self.permission_classes = [ - ProjectMemberPermission, - ] - - return super(ProjectMemberViewSet, self).get_permissions() - - search_fields = [ - "member__display_name", - "member__first_name", - ] - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter(member__is_bot=False) - .filter() - .select_related("project") - .select_related("member") - .select_related("workspace", "workspace__owner") - ) - - def create(self, request, slug, project_id): - members = request.data.get("members", []) - - # get the project - project = Project.objects.get(pk=project_id, workspace__slug=slug) - - if not len(members): - return Response( - {"error": "Atleast one member is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - bulk_project_members = [] - bulk_issue_props = [] - - project_members = ( - ProjectMember.objects.filter( - workspace__slug=slug, - member_id__in=[member.get("member_id") for member in members], - ) - .values("member_id", "sort_order") - .order_by("sort_order") - ) - - bulk_project_members = [] - member_roles = { - member.get("member_id"): member.get("role") for member in members - } - # Update roles in the members array based on the member_roles dictionary - for project_member in ProjectMember.objects.filter( - project_id=project_id, - member_id__in=[member.get("member_id") for member in members], - ): - project_member.role = member_roles[str(project_member.member_id)] - project_member.is_active = True - bulk_project_members.append(project_member) - - # Update the roles of the existing members - ProjectMember.objects.bulk_update( - bulk_project_members, ["is_active", "role"], batch_size=100 - ) - - for member in members: - sort_order = [ - project_member.get("sort_order") - for project_member in project_members - if str(project_member.get("member_id")) - == str(member.get("member_id")) - ] - bulk_project_members.append( - ProjectMember( - member_id=member.get("member_id"), - role=member.get("role", 10), - project_id=project_id, - workspace_id=project.workspace_id, - sort_order=( - sort_order[0] - 10000 if len(sort_order) else 65535 - ), - ) - ) - bulk_issue_props.append( - IssueProperty( - user_id=member.get("member_id"), - project_id=project_id, - workspace_id=project.workspace_id, - ) - ) - - project_members = ProjectMember.objects.bulk_create( - bulk_project_members, - batch_size=10, - ignore_conflicts=True, - ) - - _ = IssueProperty.objects.bulk_create( - bulk_issue_props, batch_size=10, ignore_conflicts=True - ) - - project_members = ProjectMember.objects.filter( - project_id=project_id, - member_id__in=[member.get("member_id") for member in members], - ) - serializer = ProjectMemberRoleSerializer(project_members, many=True) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - def list(self, request, slug, project_id): - # Get the list of project members for the project - project_members = ProjectMember.objects.filter( - project_id=project_id, - workspace__slug=slug, - member__is_bot=False, - is_active=True, - ).select_related("project", "member", "workspace") - - serializer = ProjectMemberRoleSerializer( - project_members, fields=("id", "member", "role"), many=True - ) - return Response(serializer.data, status=status.HTTP_200_OK) - - def partial_update(self, request, slug, project_id, pk): - project_member = ProjectMember.objects.get( - pk=pk, - workspace__slug=slug, - project_id=project_id, - is_active=True, - ) - if request.user.id == project_member.member_id: - return Response( - {"error": "You cannot update your own role"}, - status=status.HTTP_400_BAD_REQUEST, - ) - # Check while updating user roles - requested_project_member = ProjectMember.objects.get( - project_id=project_id, - workspace__slug=slug, - member=request.user, - is_active=True, - ) - if ( - "role" in request.data - and int(request.data.get("role", project_member.role)) - > requested_project_member.role - ): - return Response( - { - "error": "You cannot update a role that is higher than your own role" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - serializer = ProjectMemberSerializer( - project_member, data=request.data, partial=True - ) - - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, slug, project_id, pk): - project_member = ProjectMember.objects.get( - workspace__slug=slug, - project_id=project_id, - pk=pk, - member__is_bot=False, - is_active=True, - ) - # check requesting user role - requesting_project_member = ProjectMember.objects.get( - workspace__slug=slug, - member=request.user, - project_id=project_id, - is_active=True, - ) - # User cannot remove himself - if str(project_member.id) == str(requesting_project_member.id): - return Response( - { - "error": "You cannot remove yourself from the workspace. Please use leave workspace" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - # User cannot deactivate higher role - if requesting_project_member.role < project_member.role: - return Response( - { - "error": "You cannot remove a user having role higher than you" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - project_member.is_active = False - project_member.save() - return Response(status=status.HTTP_204_NO_CONTENT) - - def leave(self, request, slug, project_id): - project_member = ProjectMember.objects.get( - workspace__slug=slug, - project_id=project_id, - member=request.user, - is_active=True, - ) - - # Check if the leaving user is the only admin of the project - if ( - project_member.role == 20 - and not ProjectMember.objects.filter( - workspace__slug=slug, - project_id=project_id, - role=20, - is_active=True, - ).count() - > 1 - ): - return Response( - { - "error": "You cannot leave the project as your the only admin of the project you will have to either delete the project or create an another admin", - }, - status=status.HTTP_400_BAD_REQUEST, - ) - # Deactivate the user - project_member.is_active = False - project_member.save() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class AddTeamToProjectEndpoint(BaseAPIView): - permission_classes = [ - ProjectBasePermission, - ] - - def post(self, request, slug, project_id): - team_members = TeamMember.objects.filter( - workspace__slug=slug, team__in=request.data.get("teams", []) - ).values_list("member", flat=True) - - if len(team_members) == 0: - return Response( - {"error": "No such team exists"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - workspace = Workspace.objects.get(slug=slug) - - project_members = [] - issue_props = [] - for member in team_members: - project_members.append( - ProjectMember( - project_id=project_id, - member_id=member, - workspace=workspace, - created_by=request.user, - ) - ) - issue_props.append( - IssueProperty( - project_id=project_id, - user_id=member, - workspace=workspace, - created_by=request.user, - ) - ) - - ProjectMember.objects.bulk_create( - project_members, batch_size=10, ignore_conflicts=True - ) - - _ = IssueProperty.objects.bulk_create( - issue_props, batch_size=10, ignore_conflicts=True - ) - - serializer = ProjectMemberSerializer(project_members, many=True) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - -class ProjectIdentifierEndpoint(BaseAPIView): - permission_classes = [ - ProjectBasePermission, - ] - - def get(self, request, slug): - name = request.GET.get("name", "").strip().upper() - - if name == "": - return Response( - {"error": "Name is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - exists = ProjectIdentifier.objects.filter( - name=name, workspace__slug=slug - ).values("id", "name", "project") - - return Response( - {"exists": len(exists), "identifiers": exists}, - status=status.HTTP_200_OK, - ) - - def delete(self, request, slug): - name = request.data.get("name", "").strip().upper() - - if name == "": - return Response( - {"error": "Name is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - if Project.objects.filter( - identifier=name, workspace__slug=slug - ).exists(): - return Response( - { - "error": "Cannot delete an identifier of an existing project" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - ProjectIdentifier.objects.filter( - name=name, workspace__slug=slug - ).delete() - - return Response( - status=status.HTTP_204_NO_CONTENT, - ) - - -class ProjectUserViewsEndpoint(BaseAPIView): - def post(self, request, slug, project_id): - project = Project.objects.get(pk=project_id, workspace__slug=slug) - - project_member = ProjectMember.objects.filter( - member=request.user, - project=project, - is_active=True, - ).first() - - if project_member is None: - return Response( - {"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN - ) - - view_props = project_member.view_props - default_props = project_member.default_props - preferences = project_member.preferences - sort_order = project_member.sort_order - - project_member.view_props = request.data.get("view_props", view_props) - project_member.default_props = request.data.get( - "default_props", default_props - ) - project_member.preferences = request.data.get( - "preferences", preferences - ) - project_member.sort_order = request.data.get("sort_order", sort_order) - - project_member.save() - - return Response(status=status.HTTP_204_NO_CONTENT) - - -class ProjectMemberUserEndpoint(BaseAPIView): - def get(self, request, slug, project_id): - project_member = ProjectMember.objects.get( - project_id=project_id, - workspace__slug=slug, - member=request.user, - is_active=True, - ) - serializer = ProjectMemberSerializer(project_member) - - return Response(serializer.data, status=status.HTTP_200_OK) - - -class ProjectFavoritesViewSet(BaseViewSet): - serializer_class = ProjectFavoriteSerializer - model = ProjectFavorite - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(user=self.request.user) - .select_related( - "project", "project__project_lead", "project__default_assignee" - ) - .select_related("workspace", "workspace__owner") - ) - - def perform_create(self, serializer): - serializer.save(user=self.request.user) - - def create(self, request, slug): - serializer = ProjectFavoriteSerializer(data=request.data) - if serializer.is_valid(): - serializer.save(user=request.user) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, slug, project_id): - project_favorite = ProjectFavorite.objects.get( - project=project_id, user=request.user, workspace__slug=slug - ) - project_favorite.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class ProjectPublicCoverImagesEndpoint(BaseAPIView): - permission_classes = [ - AllowAny, - ] - - # Cache the below api for 24 hours - @cache_response(60 * 60 * 24, user=False) - def get(self, request): - files = [] - s3 = boto3.client( - "s3", - aws_access_key_id=settings.AWS_ACCESS_KEY_ID, - aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, - ) - params = { - "Bucket": settings.AWS_STORAGE_BUCKET_NAME, - "Prefix": "static/project-cover/", - } - - response = s3.list_objects_v2(**params) - # Extracting file keys from the response - if "Contents" in response: - for content in response["Contents"]: - if not content["Key"].endswith( - "/" - ): # This line ensures we're only getting files, not "sub-folders" - files.append( - f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}" - ) - - return Response(files, status=status.HTTP_200_OK) - - -class ProjectDeployBoardViewSet(BaseViewSet): - permission_classes = [ - ProjectMemberPermission, - ] - serializer_class = ProjectDeployBoardSerializer - model = ProjectDeployBoard - - def get_queryset(self): - return ( - super() - .get_queryset() - .filter( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), - ) - .select_related("project") - ) - - def create(self, request, slug, project_id): - comments = request.data.get("comments", False) - reactions = request.data.get("reactions", False) - inbox = request.data.get("inbox", None) - votes = request.data.get("votes", False) - views = request.data.get( - "views", - { - "list": True, - "kanban": True, - "calendar": True, - "gantt": True, - "spreadsheet": True, - }, - ) - - project_deploy_board, _ = ProjectDeployBoard.objects.get_or_create( - anchor=f"{slug}/{project_id}", - project_id=project_id, - ) - project_deploy_board.comments = comments - project_deploy_board.reactions = reactions - project_deploy_board.inbox = inbox - project_deploy_board.votes = votes - project_deploy_board.views = views - - project_deploy_board.save() - - serializer = ProjectDeployBoardSerializer(project_deploy_board) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class UserProjectRolesEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceUserPermission, - ] - - def get(self, request, slug): - project_members = ProjectMember.objects.filter( - workspace__slug=slug, - member_id=request.user.id, - ).values("project_id", "role") - - project_members = { - str(member["project_id"]): member["role"] - for member in project_members - } - return Response(project_members, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py new file mode 100644 index 000000000..6deeea144 --- /dev/null +++ b/apiserver/plane/app/views/project/base.py @@ -0,0 +1,549 @@ +# Python imports +import boto3 + +# Django imports +from django.db import IntegrityError +from django.db.models import ( + Prefetch, + Q, + Exists, + OuterRef, + F, + Func, + Subquery, +) +from django.conf import settings + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status +from rest_framework import serializers +from rest_framework.permissions import AllowAny + +# Module imports +from plane.app.views.base import BaseViewSet, BaseAPIView, WebhookMixin +from plane.app.serializers import ( + ProjectSerializer, + ProjectListSerializer, + ProjectFavoriteSerializer, + ProjectDeployBoardSerializer, +) + +from plane.app.permissions import ( + ProjectBasePermission, + ProjectMemberPermission, +) + +from plane.db.models import ( + Project, + ProjectMember, + Workspace, + State, + ProjectFavorite, + ProjectIdentifier, + Module, + Cycle, + Inbox, + ProjectDeployBoard, + IssueProperty, +) +from plane.utils.cache import cache_response + +class ProjectViewSet(WebhookMixin, BaseViewSet): + serializer_class = ProjectListSerializer + model = Project + webhook_event = "project" + + permission_classes = [ + ProjectBasePermission, + ] + + def get_queryset(self): + sort_order = ProjectMember.objects.filter( + member=self.request.user, + project_id=OuterRef("pk"), + workspace__slug=self.kwargs.get("slug"), + is_active=True, + ).values("sort_order") + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter( + Q(project_projectmember__member=self.request.user) + | Q(network=2) + ) + .select_related( + "workspace", + "workspace__owner", + "default_assignee", + "project_lead", + ) + .annotate( + is_favorite=Exists( + ProjectFavorite.objects.filter( + user=self.request.user, + project_id=OuterRef("pk"), + workspace__slug=self.kwargs.get("slug"), + ) + ) + ) + .annotate( + is_member=Exists( + ProjectMember.objects.filter( + member=self.request.user, + project_id=OuterRef("pk"), + workspace__slug=self.kwargs.get("slug"), + is_active=True, + ) + ) + ) + .annotate( + total_members=ProjectMember.objects.filter( + project_id=OuterRef("id"), + member__is_bot=False, + is_active=True, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + total_cycles=Cycle.objects.filter(project_id=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + total_modules=Module.objects.filter(project_id=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + member_role=ProjectMember.objects.filter( + project_id=OuterRef("pk"), + member_id=self.request.user.id, + is_active=True, + ).values("role") + ) + .annotate( + is_deployed=Exists( + ProjectDeployBoard.objects.filter( + project_id=OuterRef("pk"), + workspace__slug=self.kwargs.get("slug"), + ) + ) + ) + .annotate(sort_order=Subquery(sort_order)) + .prefetch_related( + Prefetch( + "project_projectmember", + queryset=ProjectMember.objects.filter( + workspace__slug=self.kwargs.get("slug"), + is_active=True, + ).select_related("member"), + to_attr="members_list", + ) + ) + .distinct() + ) + + def list(self, request, slug): + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] + projects = self.get_queryset().order_by("sort_order", "name") + if request.GET.get("per_page", False) and request.GET.get( + "cursor", False + ): + return self.paginate( + request=request, + queryset=(projects), + on_results=lambda projects: ProjectListSerializer( + projects, many=True + ).data, + ) + projects = ProjectListSerializer( + projects, many=True, fields=fields if fields else None + ).data + return Response(projects, status=status.HTTP_200_OK) + + def create(self, request, slug): + try: + workspace = Workspace.objects.get(slug=slug) + + serializer = ProjectSerializer( + data={**request.data}, context={"workspace_id": workspace.id} + ) + if serializer.is_valid(): + serializer.save() + + # Add the user as Administrator to the project + _ = ProjectMember.objects.create( + project_id=serializer.data["id"], + member=request.user, + role=20, + ) + # Also create the issue property for the user + _ = IssueProperty.objects.create( + project_id=serializer.data["id"], + user=request.user, + ) + + if serializer.data["project_lead"] is not None and str( + serializer.data["project_lead"] + ) != str(request.user.id): + ProjectMember.objects.create( + project_id=serializer.data["id"], + member_id=serializer.data["project_lead"], + role=20, + ) + # Also create the issue property for the user + IssueProperty.objects.create( + project_id=serializer.data["id"], + user_id=serializer.data["project_lead"], + ) + + # Default states + states = [ + { + "name": "Backlog", + "color": "#A3A3A3", + "sequence": 15000, + "group": "backlog", + "default": True, + }, + { + "name": "Todo", + "color": "#3A3A3A", + "sequence": 25000, + "group": "unstarted", + }, + { + "name": "In Progress", + "color": "#F59E0B", + "sequence": 35000, + "group": "started", + }, + { + "name": "Done", + "color": "#16A34A", + "sequence": 45000, + "group": "completed", + }, + { + "name": "Cancelled", + "color": "#EF4444", + "sequence": 55000, + "group": "cancelled", + }, + ] + + State.objects.bulk_create( + [ + State( + name=state["name"], + color=state["color"], + project=serializer.instance, + sequence=state["sequence"], + workspace=serializer.instance.workspace, + group=state["group"], + default=state.get("default", False), + created_by=request.user, + ) + for state in states + ] + ) + + project = ( + self.get_queryset() + .filter(pk=serializer.data["id"]) + .first() + ) + serializer = ProjectListSerializer(project) + return Response( + serializer.data, status=status.HTTP_201_CREATED + ) + return Response( + serializer.errors, + status=status.HTTP_400_BAD_REQUEST, + ) + except IntegrityError as e: + if "already exists" in str(e): + return Response( + {"name": "The project name is already taken"}, + status=status.HTTP_410_GONE, + ) + except Workspace.DoesNotExist as e: + return Response( + {"error": "Workspace does not exist"}, + status=status.HTTP_404_NOT_FOUND, + ) + except serializers.ValidationError as e: + return Response( + {"identifier": "The project identifier is already taken"}, + status=status.HTTP_410_GONE, + ) + + def partial_update(self, request, slug, pk=None): + try: + workspace = Workspace.objects.get(slug=slug) + + project = Project.objects.get(pk=pk) + + serializer = ProjectSerializer( + project, + data={**request.data}, + context={"workspace_id": workspace.id}, + partial=True, + ) + + if serializer.is_valid(): + serializer.save() + if serializer.data["inbox_view"]: + Inbox.objects.get_or_create( + name=f"{project.name} Inbox", + project=project, + is_default=True, + ) + + # Create the triage state in Backlog group + State.objects.get_or_create( + name="Triage", + group="backlog", + description="Default state for managing all Inbox Issues", + project_id=pk, + color="#ff7700", + ) + + project = ( + self.get_queryset() + .filter(pk=serializer.data["id"]) + .first() + ) + serializer = ProjectListSerializer(project) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response( + serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) + + except IntegrityError as e: + if "already exists" in str(e): + return Response( + {"name": "The project name is already taken"}, + status=status.HTTP_410_GONE, + ) + except (Project.DoesNotExist, Workspace.DoesNotExist): + return Response( + {"error": "Project does not exist"}, + status=status.HTTP_404_NOT_FOUND, + ) + except serializers.ValidationError as e: + return Response( + {"identifier": "The project identifier is already taken"}, + status=status.HTTP_410_GONE, + ) + + +class ProjectIdentifierEndpoint(BaseAPIView): + permission_classes = [ + ProjectBasePermission, + ] + + def get(self, request, slug): + name = request.GET.get("name", "").strip().upper() + + if name == "": + return Response( + {"error": "Name is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + exists = ProjectIdentifier.objects.filter( + name=name, workspace__slug=slug + ).values("id", "name", "project") + + return Response( + {"exists": len(exists), "identifiers": exists}, + status=status.HTTP_200_OK, + ) + + def delete(self, request, slug): + name = request.data.get("name", "").strip().upper() + + if name == "": + return Response( + {"error": "Name is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if Project.objects.filter( + identifier=name, workspace__slug=slug + ).exists(): + return Response( + { + "error": "Cannot delete an identifier of an existing project" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + ProjectIdentifier.objects.filter( + name=name, workspace__slug=slug + ).delete() + + return Response( + status=status.HTTP_204_NO_CONTENT, + ) + + +class ProjectUserViewsEndpoint(BaseAPIView): + def post(self, request, slug, project_id): + project = Project.objects.get(pk=project_id, workspace__slug=slug) + + project_member = ProjectMember.objects.filter( + member=request.user, + project=project, + is_active=True, + ).first() + + if project_member is None: + return Response( + {"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN + ) + + view_props = project_member.view_props + default_props = project_member.default_props + preferences = project_member.preferences + sort_order = project_member.sort_order + + project_member.view_props = request.data.get("view_props", view_props) + project_member.default_props = request.data.get( + "default_props", default_props + ) + project_member.preferences = request.data.get( + "preferences", preferences + ) + project_member.sort_order = request.data.get("sort_order", sort_order) + + project_member.save() + + return Response(status=status.HTTP_204_NO_CONTENT) + + +class ProjectFavoritesViewSet(BaseViewSet): + serializer_class = ProjectFavoriteSerializer + model = ProjectFavorite + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(user=self.request.user) + .select_related( + "project", "project__project_lead", "project__default_assignee" + ) + .select_related("workspace", "workspace__owner") + ) + + def perform_create(self, serializer): + serializer.save(user=self.request.user) + + def create(self, request, slug): + serializer = ProjectFavoriteSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(user=request.user) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id): + project_favorite = ProjectFavorite.objects.get( + project=project_id, user=request.user, workspace__slug=slug + ) + project_favorite.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class ProjectPublicCoverImagesEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + # Cache the below api for 24 hours + @cache_response(60 * 60 * 24, user=False) + def get(self, request): + files = [] + s3 = boto3.client( + "s3", + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + ) + params = { + "Bucket": settings.AWS_STORAGE_BUCKET_NAME, + "Prefix": "static/project-cover/", + } + + response = s3.list_objects_v2(**params) + # Extracting file keys from the response + if "Contents" in response: + for content in response["Contents"]: + if not content["Key"].endswith( + "/" + ): # This line ensures we're only getting files, not "sub-folders" + files.append( + f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}" + ) + + return Response(files, status=status.HTTP_200_OK) + + +class ProjectDeployBoardViewSet(BaseViewSet): + permission_classes = [ + ProjectMemberPermission, + ] + serializer_class = ProjectDeployBoardSerializer + model = ProjectDeployBoard + + def get_queryset(self): + return ( + super() + .get_queryset() + .filter( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), + ) + .select_related("project") + ) + + def create(self, request, slug, project_id): + comments = request.data.get("comments", False) + reactions = request.data.get("reactions", False) + inbox = request.data.get("inbox", None) + votes = request.data.get("votes", False) + views = request.data.get( + "views", + { + "list": True, + "kanban": True, + "calendar": True, + "gantt": True, + "spreadsheet": True, + }, + ) + + project_deploy_board, _ = ProjectDeployBoard.objects.get_or_create( + anchor=f"{slug}/{project_id}", + project_id=project_id, + ) + project_deploy_board.comments = comments + project_deploy_board.reactions = reactions + project_deploy_board.inbox = inbox + project_deploy_board.votes = votes + project_deploy_board.views = views + + project_deploy_board.save() + + serializer = ProjectDeployBoardSerializer(project_deploy_board) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/project/invite.py b/apiserver/plane/app/views/project/invite.py new file mode 100644 index 000000000..d199a8770 --- /dev/null +++ b/apiserver/plane/app/views/project/invite.py @@ -0,0 +1,286 @@ +# Python imports +import jwt +from datetime import datetime + +# Django imports +from django.core.exceptions import ValidationError +from django.core.validators import validate_email +from django.conf import settings +from django.utils import timezone + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status +from rest_framework.permissions import AllowAny + +# Module imports +from .base import BaseViewSet, BaseAPIView +from plane.app.serializers import ProjectMemberInviteSerializer + +from plane.app.permissions import ProjectBasePermission + +from plane.db.models import ( + ProjectMember, + Workspace, + ProjectMemberInvite, + User, + WorkspaceMember, + IssueProperty, +) + + +class ProjectInvitationsViewset(BaseViewSet): + serializer_class = ProjectMemberInviteSerializer + model = ProjectMemberInvite + + search_fields = [] + + permission_classes = [ + ProjectBasePermission, + ] + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .select_related("project") + .select_related("workspace", "workspace__owner") + ) + + def create(self, request, slug, project_id): + emails = request.data.get("emails", []) + + # Check if email is provided + if not emails: + return Response( + {"error": "Emails are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + requesting_user = ProjectMember.objects.get( + workspace__slug=slug, + project_id=project_id, + member_id=request.user.id, + ) + + # Check if any invited user has an higher role + if len( + [ + email + for email in emails + if int(email.get("role", 10)) > requesting_user.role + ] + ): + return Response( + {"error": "You cannot invite a user with higher role"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + workspace = Workspace.objects.get(slug=slug) + + project_invitations = [] + for email in emails: + try: + validate_email(email.get("email")) + project_invitations.append( + ProjectMemberInvite( + email=email.get("email").strip().lower(), + project_id=project_id, + workspace_id=workspace.id, + token=jwt.encode( + { + "email": email, + "timestamp": datetime.now().timestamp(), + }, + settings.SECRET_KEY, + algorithm="HS256", + ), + role=email.get("role", 10), + created_by=request.user, + ) + ) + except ValidationError: + return Response( + { + "error": f"Invalid email - {email} provided a valid email address is required to send the invite" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Create workspace member invite + project_invitations = ProjectMemberInvite.objects.bulk_create( + project_invitations, batch_size=10, ignore_conflicts=True + ) + current_site = request.META.get("HTTP_ORIGIN") + + # Send invitations + for invitation in project_invitations: + project_invitations.delay( + invitation.email, + project_id, + invitation.token, + current_site, + request.user.email, + ) + + return Response( + { + "message": "Email sent successfully", + }, + status=status.HTTP_200_OK, + ) + + +class UserProjectInvitationsViewset(BaseViewSet): + serializer_class = ProjectMemberInviteSerializer + model = ProjectMemberInvite + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(email=self.request.user.email) + .select_related("workspace", "workspace__owner", "project") + ) + + def create(self, request, slug): + project_ids = request.data.get("project_ids", []) + + # Get the workspace user role + workspace_member = WorkspaceMember.objects.get( + member=request.user, + workspace__slug=slug, + is_active=True, + ) + + workspace_role = workspace_member.role + workspace = workspace_member.workspace + + # If the user was already part of workspace + _ = ProjectMember.objects.filter( + workspace__slug=slug, + project_id__in=project_ids, + member=request.user, + ).update(is_active=True) + + ProjectMember.objects.bulk_create( + [ + ProjectMember( + project_id=project_id, + member=request.user, + role=15 if workspace_role >= 15 else 10, + workspace=workspace, + created_by=request.user, + ) + for project_id in project_ids + ], + ignore_conflicts=True, + ) + + IssueProperty.objects.bulk_create( + [ + IssueProperty( + project_id=project_id, + user=request.user, + workspace=workspace, + created_by=request.user, + ) + for project_id in project_ids + ], + ignore_conflicts=True, + ) + + return Response( + {"message": "Projects joined successfully"}, + status=status.HTTP_201_CREATED, + ) + + +class ProjectJoinEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + + def post(self, request, slug, project_id, pk): + project_invite = ProjectMemberInvite.objects.get( + pk=pk, + project_id=project_id, + workspace__slug=slug, + ) + + email = request.data.get("email", "") + + if email == "" or project_invite.email != email: + return Response( + {"error": "You do not have permission to join the project"}, + status=status.HTTP_403_FORBIDDEN, + ) + + if project_invite.responded_at is None: + project_invite.accepted = request.data.get("accepted", False) + project_invite.responded_at = timezone.now() + project_invite.save() + + if project_invite.accepted: + # Check if the user account exists + user = User.objects.filter(email=email).first() + + # Check if user is a part of workspace + workspace_member = WorkspaceMember.objects.filter( + workspace__slug=slug, member=user + ).first() + # Add him to workspace + if workspace_member is None: + _ = WorkspaceMember.objects.create( + workspace_id=project_invite.workspace_id, + member=user, + role=( + 15 + if project_invite.role >= 15 + else project_invite.role + ), + ) + else: + # Else make him active + workspace_member.is_active = True + workspace_member.save() + + # Check if the user was already a member of project then activate the user + project_member = ProjectMember.objects.filter( + workspace_id=project_invite.workspace_id, member=user + ).first() + if project_member is None: + # Create a Project Member + _ = ProjectMember.objects.create( + workspace_id=project_invite.workspace_id, + member=user, + role=project_invite.role, + ) + else: + project_member.is_active = True + project_member.role = project_member.role + project_member.save() + + return Response( + {"message": "Project Invitation Accepted"}, + status=status.HTTP_200_OK, + ) + + return Response( + {"message": "Project Invitation was not accepted"}, + status=status.HTTP_200_OK, + ) + + return Response( + {"error": "You have already responded to the invitation request"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def get(self, request, slug, project_id, pk): + project_invitation = ProjectMemberInvite.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) + serializer = ProjectMemberInviteSerializer(project_invitation) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/project/member.py b/apiserver/plane/app/views/project/member.py new file mode 100644 index 000000000..187dfc8d0 --- /dev/null +++ b/apiserver/plane/app/views/project/member.py @@ -0,0 +1,349 @@ +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .base import BaseViewSet, BaseAPIView +from plane.app.serializers import ( + ProjectMemberSerializer, + ProjectMemberAdminSerializer, + ProjectMemberRoleSerializer, +) + +from plane.app.permissions import ( + ProjectBasePermission, + ProjectMemberPermission, + ProjectLitePermission, + WorkspaceUserPermission, +) + +from plane.db.models import ( + Project, + ProjectMember, + Workspace, + TeamMember, + IssueProperty, +) + + +class ProjectMemberViewSet(BaseViewSet): + serializer_class = ProjectMemberAdminSerializer + model = ProjectMember + permission_classes = [ + ProjectMemberPermission, + ] + + def get_permissions(self): + if self.action == "leave": + self.permission_classes = [ + ProjectLitePermission, + ] + else: + self.permission_classes = [ + ProjectMemberPermission, + ] + + return super(ProjectMemberViewSet, self).get_permissions() + + search_fields = [ + "member__display_name", + "member__first_name", + ] + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(member__is_bot=False) + .filter() + .select_related("project") + .select_related("member") + .select_related("workspace", "workspace__owner") + ) + + def create(self, request, slug, project_id): + members = request.data.get("members", []) + + # get the project + project = Project.objects.get(pk=project_id, workspace__slug=slug) + + if not len(members): + return Response( + {"error": "Atleast one member is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + bulk_project_members = [] + bulk_issue_props = [] + + project_members = ( + ProjectMember.objects.filter( + workspace__slug=slug, + member_id__in=[member.get("member_id") for member in members], + ) + .values("member_id", "sort_order") + .order_by("sort_order") + ) + + bulk_project_members = [] + member_roles = { + member.get("member_id"): member.get("role") for member in members + } + # Update roles in the members array based on the member_roles dictionary + for project_member in ProjectMember.objects.filter( + project_id=project_id, + member_id__in=[member.get("member_id") for member in members], + ): + project_member.role = member_roles[str(project_member.member_id)] + project_member.is_active = True + bulk_project_members.append(project_member) + + # Update the roles of the existing members + ProjectMember.objects.bulk_update( + bulk_project_members, ["is_active", "role"], batch_size=100 + ) + + for member in members: + sort_order = [ + project_member.get("sort_order") + for project_member in project_members + if str(project_member.get("member_id")) + == str(member.get("member_id")) + ] + bulk_project_members.append( + ProjectMember( + member_id=member.get("member_id"), + role=member.get("role", 10), + project_id=project_id, + workspace_id=project.workspace_id, + sort_order=( + sort_order[0] - 10000 if len(sort_order) else 65535 + ), + ) + ) + bulk_issue_props.append( + IssueProperty( + user_id=member.get("member_id"), + project_id=project_id, + workspace_id=project.workspace_id, + ) + ) + + project_members = ProjectMember.objects.bulk_create( + bulk_project_members, + batch_size=10, + ignore_conflicts=True, + ) + + _ = IssueProperty.objects.bulk_create( + bulk_issue_props, batch_size=10, ignore_conflicts=True + ) + + project_members = ProjectMember.objects.filter( + project_id=project_id, + member_id__in=[member.get("member_id") for member in members], + ) + serializer = ProjectMemberRoleSerializer(project_members, many=True) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def list(self, request, slug, project_id): + # Get the list of project members for the project + project_members = ProjectMember.objects.filter( + project_id=project_id, + workspace__slug=slug, + member__is_bot=False, + is_active=True, + ).select_related("project", "member", "workspace") + + serializer = ProjectMemberRoleSerializer( + project_members, fields=("id", "member", "role"), many=True + ) + return Response(serializer.data, status=status.HTTP_200_OK) + + def partial_update(self, request, slug, project_id, pk): + project_member = ProjectMember.objects.get( + pk=pk, + workspace__slug=slug, + project_id=project_id, + is_active=True, + ) + if request.user.id == project_member.member_id: + return Response( + {"error": "You cannot update your own role"}, + status=status.HTTP_400_BAD_REQUEST, + ) + # Check while updating user roles + requested_project_member = ProjectMember.objects.get( + project_id=project_id, + workspace__slug=slug, + member=request.user, + is_active=True, + ) + if ( + "role" in request.data + and int(request.data.get("role", project_member.role)) + > requested_project_member.role + ): + return Response( + { + "error": "You cannot update a role that is higher than your own role" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + serializer = ProjectMemberSerializer( + project_member, data=request.data, partial=True + ) + + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, pk): + project_member = ProjectMember.objects.get( + workspace__slug=slug, + project_id=project_id, + pk=pk, + member__is_bot=False, + is_active=True, + ) + # check requesting user role + requesting_project_member = ProjectMember.objects.get( + workspace__slug=slug, + member=request.user, + project_id=project_id, + is_active=True, + ) + # User cannot remove himself + if str(project_member.id) == str(requesting_project_member.id): + return Response( + { + "error": "You cannot remove yourself from the workspace. Please use leave workspace" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + # User cannot deactivate higher role + if requesting_project_member.role < project_member.role: + return Response( + { + "error": "You cannot remove a user having role higher than you" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + project_member.is_active = False + project_member.save() + return Response(status=status.HTTP_204_NO_CONTENT) + + def leave(self, request, slug, project_id): + project_member = ProjectMember.objects.get( + workspace__slug=slug, + project_id=project_id, + member=request.user, + is_active=True, + ) + + # Check if the leaving user is the only admin of the project + if ( + project_member.role == 20 + and not ProjectMember.objects.filter( + workspace__slug=slug, + project_id=project_id, + role=20, + is_active=True, + ).count() + > 1 + ): + return Response( + { + "error": "You cannot leave the project as your the only admin of the project you will have to either delete the project or create an another admin", + }, + status=status.HTTP_400_BAD_REQUEST, + ) + # Deactivate the user + project_member.is_active = False + project_member.save() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class AddTeamToProjectEndpoint(BaseAPIView): + permission_classes = [ + ProjectBasePermission, + ] + + def post(self, request, slug, project_id): + team_members = TeamMember.objects.filter( + workspace__slug=slug, team__in=request.data.get("teams", []) + ).values_list("member", flat=True) + + if len(team_members) == 0: + return Response( + {"error": "No such team exists"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + workspace = Workspace.objects.get(slug=slug) + + project_members = [] + issue_props = [] + for member in team_members: + project_members.append( + ProjectMember( + project_id=project_id, + member_id=member, + workspace=workspace, + created_by=request.user, + ) + ) + issue_props.append( + IssueProperty( + project_id=project_id, + user_id=member, + workspace=workspace, + created_by=request.user, + ) + ) + + ProjectMember.objects.bulk_create( + project_members, batch_size=10, ignore_conflicts=True + ) + + _ = IssueProperty.objects.bulk_create( + issue_props, batch_size=10, ignore_conflicts=True + ) + + serializer = ProjectMemberSerializer(project_members, many=True) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + +class ProjectMemberUserEndpoint(BaseAPIView): + def get(self, request, slug, project_id): + project_member = ProjectMember.objects.get( + project_id=project_id, + workspace__slug=slug, + member=request.user, + is_active=True, + ) + serializer = ProjectMemberSerializer(project_member) + + return Response(serializer.data, status=status.HTTP_200_OK) + + +class UserProjectRolesEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceUserPermission, + ] + + def get(self, request, slug): + project_members = ProjectMember.objects.filter( + workspace__slug=slug, + member_id=request.user.id, + ).values("project_id", "role") + + project_members = { + str(member["project_id"]): member["role"] + for member in project_members + } + return Response(project_members, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/state.py b/apiserver/plane/app/views/state/base.py similarity index 99% rename from apiserver/plane/app/views/state.py rename to apiserver/plane/app/views/state/base.py index 6d4fd7782..137a89d99 100644 --- a/apiserver/plane/app/views/state.py +++ b/apiserver/plane/app/views/state/base.py @@ -9,7 +9,7 @@ from rest_framework.response import Response from rest_framework import status # Module imports -from . import BaseViewSet +from .. import BaseViewSet from plane.app.serializers import StateSerializer from plane.app.permissions import ( ProjectEntityPermission, diff --git a/apiserver/plane/app/views/user.py b/apiserver/plane/app/views/user/base.py similarity index 100% rename from apiserver/plane/app/views/user.py rename to apiserver/plane/app/views/user/base.py diff --git a/apiserver/plane/app/views/view.py b/apiserver/plane/app/views/view/base.py similarity index 99% rename from apiserver/plane/app/views/view.py rename to apiserver/plane/app/views/view/base.py index 3461f78f6..16c50e880 100644 --- a/apiserver/plane/app/views/view.py +++ b/apiserver/plane/app/views/view/base.py @@ -23,7 +23,7 @@ from rest_framework.response import Response from rest_framework import status # Module imports -from . import BaseViewSet +from .. import BaseViewSet from plane.app.serializers import ( IssueViewSerializer, IssueSerializer, diff --git a/apiserver/plane/app/views/webhook.py b/apiserver/plane/app/views/webhook/base.py similarity index 99% rename from apiserver/plane/app/views/webhook.py rename to apiserver/plane/app/views/webhook/base.py index 6c110eea3..9586722a0 100644 --- a/apiserver/plane/app/views/webhook.py +++ b/apiserver/plane/app/views/webhook/base.py @@ -8,7 +8,7 @@ from rest_framework.response import Response # Module imports from plane.db.models import Webhook, WebhookLog, Workspace from plane.db.models.webhook import generate_token -from .base import BaseAPIView +from ..base import BaseAPIView from plane.app.permissions import WorkspaceOwnerPermission from plane.app.serializers import WebhookSerializer, WebhookLogSerializer diff --git a/apiserver/plane/app/views/workspace.py b/apiserver/plane/app/views/workspace.py deleted file mode 100644 index 419901062..000000000 --- a/apiserver/plane/app/views/workspace.py +++ /dev/null @@ -1,1843 +0,0 @@ -# Python imports -import jwt -import csv -import io -from datetime import date, datetime -from dateutil.relativedelta import relativedelta - -# Django imports -from django.http import HttpResponse -from django.db import IntegrityError -from django.conf import settings -from django.utils import timezone -from django.core.exceptions import ValidationError -from django.core.validators import validate_email -from django.db.models import ( - Prefetch, - OuterRef, - Func, - F, - Q, - Count, - Case, - Value, - CharField, - When, - Max, - IntegerField, - Sum, -) -from django.db.models.functions import ExtractWeek, Cast, ExtractDay -from django.db.models.fields import DateField -from django.contrib.postgres.aggregates import ArrayAgg -from django.contrib.postgres.fields import ArrayField -from django.db.models import UUIDField -from django.db.models.functions import Coalesce - -# Third party modules -from rest_framework import status -from rest_framework.response import Response -from rest_framework.permissions import AllowAny - -# Module imports -from plane.app.serializers import ( - WorkSpaceSerializer, - WorkSpaceMemberSerializer, - TeamSerializer, - WorkSpaceMemberInviteSerializer, - UserLiteSerializer, - ProjectMemberSerializer, - WorkspaceThemeSerializer, - IssueActivitySerializer, - IssueSerializer, - WorkspaceMemberAdminSerializer, - WorkspaceMemberMeSerializer, - ProjectMemberRoleSerializer, - WorkspaceUserPropertiesSerializer, - WorkspaceEstimateSerializer, - StateSerializer, - LabelSerializer, - CycleSerializer, - ModuleSerializer, -) -from plane.app.views.base import BaseAPIView -from . import BaseViewSet -from plane.db.models import ( - State, - User, - Workspace, - WorkspaceMemberInvite, - Team, - ProjectMember, - IssueActivity, - Issue, - WorkspaceTheme, - IssueLink, - IssueAttachment, - IssueSubscriber, - Project, - Label, - WorkspaceMember, - CycleIssue, - WorkspaceUserProperties, - Estimate, - EstimatePoint, - Module, - ModuleLink, - Cycle, -) -from plane.app.permissions import ( - WorkSpaceBasePermission, - WorkSpaceAdminPermission, - WorkspaceEntityPermission, - WorkspaceViewerPermission, - WorkspaceUserPermission, -) -from plane.bgtasks.workspace_invitation_task import workspace_invitation -from plane.utils.issue_filters import issue_filters -from plane.bgtasks.event_tracking_task import workspace_invite_event -from plane.utils.cache import cache_response, invalidate_cache - - -class WorkSpaceViewSet(BaseViewSet): - model = Workspace - serializer_class = WorkSpaceSerializer - permission_classes = [ - WorkSpaceBasePermission, - ] - - search_fields = [ - "name", - ] - filterset_fields = [ - "owner", - ] - - lookup_field = "slug" - - def get_queryset(self): - member_count = ( - WorkspaceMember.objects.filter( - workspace=OuterRef("id"), - member__is_bot=False, - is_active=True, - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - - issue_count = ( - Issue.issue_objects.filter(workspace=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - return ( - self.filter_queryset( - super().get_queryset().select_related("owner") - ) - .order_by("name") - .filter( - workspace_member__member=self.request.user, - workspace_member__is_active=True, - ) - .annotate(total_members=member_count) - .annotate(total_issues=issue_count) - .select_related("owner") - ) - @invalidate_cache(path="/api/workspaces/", user=False) - @invalidate_cache(path="/api/users/me/workspaces/") - def create(self, request): - try: - serializer = WorkSpaceSerializer(data=request.data) - - slug = request.data.get("slug", False) - name = request.data.get("name", False) - - if not name or not slug: - return Response( - {"error": "Both name and slug are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - if len(name) > 80 or len(slug) > 48: - return Response( - { - "error": "The maximum length for name is 80 and for slug is 48" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - if serializer.is_valid(): - serializer.save(owner=request.user) - # Create Workspace member - _ = WorkspaceMember.objects.create( - workspace_id=serializer.data["id"], - member=request.user, - role=20, - company_role=request.data.get("company_role", ""), - ) - return Response( - serializer.data, status=status.HTTP_201_CREATED - ) - return Response( - [serializer.errors[error][0] for error in serializer.errors], - status=status.HTTP_400_BAD_REQUEST, - ) - - except IntegrityError as e: - if "already exists" in str(e): - return Response( - {"slug": "The workspace with the slug already exists"}, - status=status.HTTP_410_GONE, - ) - - @cache_response(60 * 60 * 2) - def list(self, request, *args, **kwargs): - return super().list(request, *args, **kwargs) - - @invalidate_cache(path="/api/workspaces/", user=False) - @invalidate_cache(path="/api/users/me/workspaces/") - def partial_update(self, request, *args, **kwargs): - return super().partial_update(request, *args, **kwargs) - - @invalidate_cache(path="/api/workspaces/", user=False) - @invalidate_cache(path="/api/users/me/workspaces/") - def destroy(self, request, *args, **kwargs): - return super().destroy(request, *args, **kwargs) - - -class UserWorkSpacesEndpoint(BaseAPIView): - search_fields = [ - "name", - ] - filterset_fields = [ - "owner", - ] - - @cache_response(60 * 60 * 2) - def get(self, request): - fields = [ - field - for field in request.GET.get("fields", "").split(",") - if field - ] - member_count = ( - WorkspaceMember.objects.filter( - workspace=OuterRef("id"), - member__is_bot=False, - is_active=True, - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - - issue_count = ( - Issue.issue_objects.filter(workspace=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - - workspace = ( - Workspace.objects.prefetch_related( - Prefetch( - "workspace_member", - queryset=WorkspaceMember.objects.filter( - member=request.user, is_active=True - ), - ) - ) - .select_related("owner") - .annotate(total_members=member_count) - .annotate(total_issues=issue_count) - .filter( - workspace_member__member=request.user, - workspace_member__is_active=True, - ) - .distinct() - ) - workspaces = WorkSpaceSerializer( - self.filter_queryset(workspace), - fields=fields if fields else None, - many=True, - ).data - return Response(workspaces, status=status.HTTP_200_OK) - - -class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView): - def get(self, request): - slug = request.GET.get("slug", False) - - if not slug or slug == "": - return Response( - {"error": "Workspace Slug is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - workspace = Workspace.objects.filter(slug=slug).exists() - return Response({"status": not workspace}, status=status.HTTP_200_OK) - - -class WorkspaceInvitationsViewset(BaseViewSet): - """Endpoint for creating, listing and deleting workspaces""" - - serializer_class = WorkSpaceMemberInviteSerializer - model = WorkspaceMemberInvite - - permission_classes = [ - WorkSpaceAdminPermission, - ] - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .select_related("workspace", "workspace__owner", "created_by") - ) - - def create(self, request, slug): - emails = request.data.get("emails", []) - # Check if email is provided - if not emails: - return Response( - {"error": "Emails are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # check for role level of the requesting user - requesting_user = WorkspaceMember.objects.get( - workspace__slug=slug, - member=request.user, - is_active=True, - ) - - # Check if any invited user has an higher role - if len( - [ - email - for email in emails - if int(email.get("role", 10)) > requesting_user.role - ] - ): - return Response( - {"error": "You cannot invite a user with higher role"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Get the workspace object - workspace = Workspace.objects.get(slug=slug) - - # Check if user is already a member of workspace - workspace_members = WorkspaceMember.objects.filter( - workspace_id=workspace.id, - member__email__in=[email.get("email") for email in emails], - is_active=True, - ).select_related("member", "workspace", "workspace__owner") - - if workspace_members: - return Response( - { - "error": "Some users are already member of workspace", - "workspace_users": WorkSpaceMemberSerializer( - workspace_members, many=True - ).data, - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - workspace_invitations = [] - for email in emails: - try: - validate_email(email.get("email")) - workspace_invitations.append( - WorkspaceMemberInvite( - email=email.get("email").strip().lower(), - workspace_id=workspace.id, - token=jwt.encode( - { - "email": email, - "timestamp": datetime.now().timestamp(), - }, - settings.SECRET_KEY, - algorithm="HS256", - ), - role=email.get("role", 10), - created_by=request.user, - ) - ) - except ValidationError: - return Response( - { - "error": f"Invalid email - {email} provided a valid email address is required to send the invite" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - # Create workspace member invite - workspace_invitations = WorkspaceMemberInvite.objects.bulk_create( - workspace_invitations, batch_size=10, ignore_conflicts=True - ) - - current_site = request.META.get("HTTP_ORIGIN") - - # Send invitations - for invitation in workspace_invitations: - workspace_invitation.delay( - invitation.email, - workspace.id, - invitation.token, - current_site, - request.user.email, - ) - - return Response( - { - "message": "Emails sent successfully", - }, - status=status.HTTP_200_OK, - ) - - def destroy(self, request, slug, pk): - workspace_member_invite = WorkspaceMemberInvite.objects.get( - pk=pk, workspace__slug=slug - ) - workspace_member_invite.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class WorkspaceJoinEndpoint(BaseAPIView): - permission_classes = [ - AllowAny, - ] - """Invitation response endpoint the user can respond to the invitation""" - - @invalidate_cache(path="/api/workspaces/", user=False) - @invalidate_cache(path="/api/users/me/workspaces/") - def post(self, request, slug, pk): - workspace_invite = WorkspaceMemberInvite.objects.get( - pk=pk, workspace__slug=slug - ) - - email = request.data.get("email", "") - - # Check the email - if email == "" or workspace_invite.email != email: - return Response( - {"error": "You do not have permission to join the workspace"}, - status=status.HTTP_403_FORBIDDEN, - ) - - # If already responded then return error - if workspace_invite.responded_at is None: - workspace_invite.accepted = request.data.get("accepted", False) - workspace_invite.responded_at = timezone.now() - workspace_invite.save() - - if workspace_invite.accepted: - # Check if the user created account after invitation - user = User.objects.filter(email=email).first() - - # If the user is present then create the workspace member - if user is not None: - # Check if the user was already a member of workspace then activate the user - workspace_member = WorkspaceMember.objects.filter( - workspace=workspace_invite.workspace, member=user - ).first() - if workspace_member is not None: - workspace_member.is_active = True - workspace_member.role = workspace_invite.role - workspace_member.save() - else: - # Create a Workspace - _ = WorkspaceMember.objects.create( - workspace=workspace_invite.workspace, - member=user, - role=workspace_invite.role, - ) - - # Set the user last_workspace_id to the accepted workspace - user.last_workspace_id = workspace_invite.workspace.id - user.save() - - # Delete the invitation - workspace_invite.delete() - - # Send event - workspace_invite_event.delay( - user=user.id if user is not None else None, - email=email, - user_agent=request.META.get("HTTP_USER_AGENT"), - ip=request.META.get("REMOTE_ADDR"), - event_name="MEMBER_ACCEPTED", - accepted_from="EMAIL", - ) - - return Response( - {"message": "Workspace Invitation Accepted"}, - status=status.HTTP_200_OK, - ) - - # Workspace invitation rejected - return Response( - {"message": "Workspace Invitation was not accepted"}, - status=status.HTTP_200_OK, - ) - - return Response( - {"error": "You have already responded to the invitation request"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - def get(self, request, slug, pk): - workspace_invitation = WorkspaceMemberInvite.objects.get( - workspace__slug=slug, pk=pk - ) - serializer = WorkSpaceMemberInviteSerializer(workspace_invitation) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class UserWorkspaceInvitationsViewSet(BaseViewSet): - serializer_class = WorkSpaceMemberInviteSerializer - model = WorkspaceMemberInvite - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(email=self.request.user.email) - .select_related("workspace", "workspace__owner", "created_by") - .annotate(total_members=Count("workspace__workspace_member")) - ) - - @invalidate_cache(path="/api/workspaces/", user=False) - @invalidate_cache(path="/api/users/me/workspaces/") - @invalidate_cache(path="/api/workspaces/:slug/members/", url_params=True, user=False) - def create(self, request): - invitations = request.data.get("invitations", []) - workspace_invitations = WorkspaceMemberInvite.objects.filter( - pk__in=invitations, email=request.user.email - ).order_by("-created_at") - - # If the user is already a member of workspace and was deactivated then activate the user - for invitation in workspace_invitations: - # Update the WorkspaceMember for this specific invitation - WorkspaceMember.objects.filter( - workspace_id=invitation.workspace_id, member=request.user - ).update(is_active=True, role=invitation.role) - - # Bulk create the user for all the workspaces - WorkspaceMember.objects.bulk_create( - [ - WorkspaceMember( - workspace=invitation.workspace, - member=request.user, - role=invitation.role, - created_by=request.user, - ) - for invitation in workspace_invitations - ], - ignore_conflicts=True, - ) - - # Delete joined workspace invites - workspace_invitations.delete() - - return Response(status=status.HTTP_204_NO_CONTENT) - - -class WorkSpaceMemberViewSet(BaseViewSet): - serializer_class = WorkspaceMemberAdminSerializer - model = WorkspaceMember - - permission_classes = [ - WorkspaceEntityPermission, - ] - - def get_permissions(self): - if self.action == "leave": - self.permission_classes = [ - WorkspaceUserPermission, - ] - else: - self.permission_classes = [ - WorkspaceEntityPermission, - ] - - return super(WorkSpaceMemberViewSet, self).get_permissions() - - search_fields = [ - "member__display_name", - "member__first_name", - ] - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter( - workspace__slug=self.kwargs.get("slug"), - is_active=True, - ) - .select_related("workspace", "workspace__owner") - .select_related("member") - ) - - @cache_response(60 * 60 * 2) - def list(self, request, slug): - workspace_member = WorkspaceMember.objects.get( - member=request.user, - workspace__slug=slug, - is_active=True, - ) - - # Get all active workspace members - workspace_members = self.get_queryset() - - if workspace_member.role > 10: - serializer = WorkspaceMemberAdminSerializer( - workspace_members, - fields=("id", "member", "role"), - many=True, - ) - else: - serializer = WorkSpaceMemberSerializer( - workspace_members, - fields=("id", "member", "role"), - many=True, - ) - return Response(serializer.data, status=status.HTTP_200_OK) - - @invalidate_cache(path="/api/workspaces/:slug/members/", url_params=True, user=False) - def partial_update(self, request, slug, pk): - workspace_member = WorkspaceMember.objects.get( - pk=pk, - workspace__slug=slug, - member__is_bot=False, - is_active=True, - ) - if request.user.id == workspace_member.member_id: - return Response( - {"error": "You cannot update your own role"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Get the requested user role - requested_workspace_member = WorkspaceMember.objects.get( - workspace__slug=slug, - member=request.user, - is_active=True, - ) - # Check if role is being updated - # One cannot update role higher than his own role - if ( - "role" in request.data - and int(request.data.get("role", workspace_member.role)) - > requested_workspace_member.role - ): - return Response( - { - "error": "You cannot update a role that is higher than your own role" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - serializer = WorkSpaceMemberSerializer( - workspace_member, data=request.data, partial=True - ) - - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - @invalidate_cache(path="/api/workspaces/:slug/members/", url_params=True, user=False) - def destroy(self, request, slug, pk): - # Check the user role who is deleting the user - workspace_member = WorkspaceMember.objects.get( - workspace__slug=slug, - pk=pk, - member__is_bot=False, - is_active=True, - ) - - # check requesting user role - requesting_workspace_member = WorkspaceMember.objects.get( - workspace__slug=slug, - member=request.user, - is_active=True, - ) - - if str(workspace_member.id) == str(requesting_workspace_member.id): - return Response( - { - "error": "You cannot remove yourself from the workspace. Please use leave workspace" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - if requesting_workspace_member.role < workspace_member.role: - return Response( - { - "error": "You cannot remove a user having role higher than you" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - if ( - Project.objects.annotate( - total_members=Count("project_projectmember"), - member_with_role=Count( - "project_projectmember", - filter=Q( - project_projectmember__member_id=workspace_member.id, - project_projectmember__role=20, - ), - ), - ) - .filter(total_members=1, member_with_role=1, workspace__slug=slug) - .exists() - ): - return Response( - { - "error": "User is a part of some projects where they are the only admin, they should either leave that project or promote another user to admin." - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Deactivate the users from the projects where the user is part of - _ = ProjectMember.objects.filter( - workspace__slug=slug, - member_id=workspace_member.member_id, - is_active=True, - ).update(is_active=False) - - workspace_member.is_active = False - workspace_member.save() - return Response(status=status.HTTP_204_NO_CONTENT) - - @invalidate_cache(path="/api/workspaces/:slug/members/", url_params=True, user=False) - def leave(self, request, slug): - workspace_member = WorkspaceMember.objects.get( - workspace__slug=slug, - member=request.user, - is_active=True, - ) - - # Check if the leaving user is the only admin of the workspace - if ( - workspace_member.role == 20 - and not WorkspaceMember.objects.filter( - workspace__slug=slug, - role=20, - is_active=True, - ).count() - > 1 - ): - return Response( - { - "error": "You cannot leave the workspace as you are the only admin of the workspace you will have to either delete the workspace or promote another user to admin." - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - if ( - Project.objects.annotate( - total_members=Count("project_projectmember"), - member_with_role=Count( - "project_projectmember", - filter=Q( - project_projectmember__member_id=request.user.id, - project_projectmember__role=20, - ), - ), - ) - .filter(total_members=1, member_with_role=1, workspace__slug=slug) - .exists() - ): - return Response( - { - "error": "You are a part of some projects where you are the only admin, you should either leave the project or promote another user to admin." - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - # # Deactivate the users from the projects where the user is part of - _ = ProjectMember.objects.filter( - workspace__slug=slug, - member_id=workspace_member.member_id, - is_active=True, - ).update(is_active=False) - - # # Deactivate the user - workspace_member.is_active = False - workspace_member.save() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class WorkspaceProjectMemberEndpoint(BaseAPIView): - serializer_class = ProjectMemberRoleSerializer - model = ProjectMember - - permission_classes = [ - WorkspaceEntityPermission, - ] - - def get(self, request, slug): - # Fetch all project IDs where the user is involved - project_ids = ( - ProjectMember.objects.filter( - member=request.user, - is_active=True, - ) - .values_list("project_id", flat=True) - .distinct() - ) - - # Get all the project members in which the user is involved - project_members = ProjectMember.objects.filter( - workspace__slug=slug, - project_id__in=project_ids, - is_active=True, - ).select_related("project", "member", "workspace") - project_members = ProjectMemberRoleSerializer( - project_members, many=True - ).data - - project_members_dict = dict() - - # Construct a dictionary with project_id as key and project_members as value - for project_member in project_members: - project_id = project_member.pop("project") - if str(project_id) not in project_members_dict: - project_members_dict[str(project_id)] = [] - project_members_dict[str(project_id)].append(project_member) - - return Response(project_members_dict, status=status.HTTP_200_OK) - - -class TeamMemberViewSet(BaseViewSet): - serializer_class = TeamSerializer - model = Team - permission_classes = [ - WorkSpaceAdminPermission, - ] - - search_fields = [ - "member__display_name", - "member__first_name", - ] - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .select_related("workspace", "workspace__owner") - .prefetch_related("members") - ) - - def create(self, request, slug): - members = list( - WorkspaceMember.objects.filter( - workspace__slug=slug, - member__id__in=request.data.get("members", []), - is_active=True, - ) - .annotate(member_str_id=Cast("member", output_field=CharField())) - .distinct() - .values_list("member_str_id", flat=True) - ) - - if len(members) != len(request.data.get("members", [])): - users = list( - set(request.data.get("members", [])).difference(members) - ) - users = User.objects.filter(pk__in=users) - - serializer = UserLiteSerializer(users, many=True) - return Response( - { - "error": f"{len(users)} of the member(s) are not a part of the workspace", - "members": serializer.data, - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - workspace = Workspace.objects.get(slug=slug) - - serializer = TeamSerializer( - data=request.data, context={"workspace": workspace} - ) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -class UserLastProjectWithWorkspaceEndpoint(BaseAPIView): - def get(self, request): - user = User.objects.get(pk=request.user.id) - - last_workspace_id = user.last_workspace_id - - if last_workspace_id is None: - return Response( - { - "project_details": [], - "workspace_details": {}, - }, - status=status.HTTP_200_OK, - ) - - workspace = Workspace.objects.get(pk=last_workspace_id) - workspace_serializer = WorkSpaceSerializer(workspace) - - project_member = ProjectMember.objects.filter( - workspace_id=last_workspace_id, member=request.user - ).select_related("workspace", "project", "member", "workspace__owner") - - project_member_serializer = ProjectMemberSerializer( - project_member, many=True - ) - - return Response( - { - "workspace_details": workspace_serializer.data, - "project_details": project_member_serializer.data, - }, - status=status.HTTP_200_OK, - ) - - -class WorkspaceMemberUserEndpoint(BaseAPIView): - def get(self, request, slug): - workspace_member = WorkspaceMember.objects.get( - member=request.user, - workspace__slug=slug, - is_active=True, - ) - serializer = WorkspaceMemberMeSerializer(workspace_member) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class WorkspaceMemberUserViewsEndpoint(BaseAPIView): - def post(self, request, slug): - workspace_member = WorkspaceMember.objects.get( - workspace__slug=slug, - member=request.user, - is_active=True, - ) - workspace_member.view_props = request.data.get("view_props", {}) - workspace_member.save() - - return Response(status=status.HTTP_204_NO_CONTENT) - - -class UserActivityGraphEndpoint(BaseAPIView): - def get(self, request, slug): - issue_activities = ( - IssueActivity.objects.filter( - actor=request.user, - workspace__slug=slug, - created_at__date__gte=date.today() + relativedelta(months=-6), - ) - .annotate(created_date=Cast("created_at", DateField())) - .values("created_date") - .annotate(activity_count=Count("created_date")) - .order_by("created_date") - ) - - return Response(issue_activities, status=status.HTTP_200_OK) - - -class UserIssueCompletedGraphEndpoint(BaseAPIView): - def get(self, request, slug): - month = request.GET.get("month", 1) - - issues = ( - Issue.issue_objects.filter( - assignees__in=[request.user], - workspace__slug=slug, - completed_at__month=month, - completed_at__isnull=False, - ) - .annotate(completed_week=ExtractWeek("completed_at")) - .annotate(week=F("completed_week") % 4) - .values("week") - .annotate(completed_count=Count("completed_week")) - .order_by("week") - ) - - return Response(issues, status=status.HTTP_200_OK) - - -class WeekInMonth(Func): - function = "FLOOR" - template = "(((%(expressions)s - 1) / 7) + 1)::INTEGER" - - -class UserWorkspaceDashboardEndpoint(BaseAPIView): - def get(self, request, slug): - issue_activities = ( - IssueActivity.objects.filter( - actor=request.user, - workspace__slug=slug, - created_at__date__gte=date.today() + relativedelta(months=-3), - ) - .annotate(created_date=Cast("created_at", DateField())) - .values("created_date") - .annotate(activity_count=Count("created_date")) - .order_by("created_date") - ) - - month = request.GET.get("month", 1) - - completed_issues = ( - Issue.issue_objects.filter( - assignees__in=[request.user], - workspace__slug=slug, - completed_at__month=month, - completed_at__isnull=False, - ) - .annotate(day_of_month=ExtractDay("completed_at")) - .annotate(week_in_month=WeekInMonth(F("day_of_month"))) - .values("week_in_month") - .annotate(completed_count=Count("id")) - .order_by("week_in_month") - ) - - assigned_issues = Issue.issue_objects.filter( - workspace__slug=slug, assignees__in=[request.user] - ).count() - - pending_issues_count = Issue.issue_objects.filter( - ~Q(state__group__in=["completed", "cancelled"]), - workspace__slug=slug, - assignees__in=[request.user], - ).count() - - completed_issues_count = Issue.issue_objects.filter( - workspace__slug=slug, - assignees__in=[request.user], - state__group="completed", - ).count() - - issues_due_week = ( - Issue.issue_objects.filter( - workspace__slug=slug, - assignees__in=[request.user], - ) - .annotate(target_week=ExtractWeek("target_date")) - .filter(target_week=timezone.now().date().isocalendar()[1]) - .count() - ) - - state_distribution = ( - Issue.issue_objects.filter( - workspace__slug=slug, assignees__in=[request.user] - ) - .annotate(state_group=F("state__group")) - .values("state_group") - .annotate(state_count=Count("state_group")) - .order_by("state_group") - ) - - overdue_issues = Issue.issue_objects.filter( - ~Q(state__group__in=["completed", "cancelled"]), - workspace__slug=slug, - assignees__in=[request.user], - target_date__lt=timezone.now(), - completed_at__isnull=True, - ).values("id", "name", "workspace__slug", "project_id", "target_date") - - upcoming_issues = Issue.issue_objects.filter( - ~Q(state__group__in=["completed", "cancelled"]), - start_date__gte=timezone.now(), - workspace__slug=slug, - assignees__in=[request.user], - completed_at__isnull=True, - ).values("id", "name", "workspace__slug", "project_id", "start_date") - - return Response( - { - "issue_activities": issue_activities, - "completed_issues": completed_issues, - "assigned_issues_count": assigned_issues, - "pending_issues_count": pending_issues_count, - "completed_issues_count": completed_issues_count, - "issues_due_week_count": issues_due_week, - "state_distribution": state_distribution, - "overdue_issues": overdue_issues, - "upcoming_issues": upcoming_issues, - }, - status=status.HTTP_200_OK, - ) - - -class WorkspaceThemeViewSet(BaseViewSet): - permission_classes = [ - WorkSpaceAdminPermission, - ] - model = WorkspaceTheme - serializer_class = WorkspaceThemeSerializer - - def get_queryset(self): - return ( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - ) - - def create(self, request, slug): - workspace = Workspace.objects.get(slug=slug) - serializer = WorkspaceThemeSerializer(data=request.data) - if serializer.is_valid(): - serializer.save(workspace=workspace, actor=request.user) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -class WorkspaceUserProfileStatsEndpoint(BaseAPIView): - def get(self, request, slug, user_id): - filters = issue_filters(request.query_params, "GET") - - state_distribution = ( - Issue.issue_objects.filter( - workspace__slug=slug, - assignees__in=[user_id], - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - ) - .filter(**filters) - .annotate(state_group=F("state__group")) - .values("state_group") - .annotate(state_count=Count("state_group")) - .order_by("state_group") - ) - - priority_order = ["urgent", "high", "medium", "low", "none"] - - priority_distribution = ( - Issue.issue_objects.filter( - workspace__slug=slug, - assignees__in=[user_id], - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - ) - .filter(**filters) - .values("priority") - .annotate(priority_count=Count("priority")) - .filter(priority_count__gte=1) - .annotate( - priority_order=Case( - *[ - When(priority=p, then=Value(i)) - for i, p in enumerate(priority_order) - ], - default=Value(len(priority_order)), - output_field=IntegerField(), - ) - ) - .order_by("priority_order") - ) - - created_issues = ( - Issue.issue_objects.filter( - workspace__slug=slug, - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - created_by_id=user_id, - ) - .filter(**filters) - .count() - ) - - assigned_issues_count = ( - Issue.issue_objects.filter( - workspace__slug=slug, - assignees__in=[user_id], - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - ) - .filter(**filters) - .count() - ) - - pending_issues_count = ( - Issue.issue_objects.filter( - ~Q(state__group__in=["completed", "cancelled"]), - workspace__slug=slug, - assignees__in=[user_id], - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - ) - .filter(**filters) - .count() - ) - - completed_issues_count = ( - Issue.issue_objects.filter( - workspace__slug=slug, - assignees__in=[user_id], - state__group="completed", - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - ) - .filter(**filters) - .count() - ) - - subscribed_issues_count = ( - IssueSubscriber.objects.filter( - workspace__slug=slug, - subscriber_id=user_id, - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - ) - .filter(**filters) - .count() - ) - - upcoming_cycles = CycleIssue.objects.filter( - workspace__slug=slug, - cycle__start_date__gt=timezone.now().date(), - issue__assignees__in=[ - user_id, - ], - ).values("cycle__name", "cycle__id", "cycle__project_id") - - present_cycle = CycleIssue.objects.filter( - workspace__slug=slug, - cycle__start_date__lt=timezone.now().date(), - cycle__end_date__gt=timezone.now().date(), - issue__assignees__in=[ - user_id, - ], - ).values("cycle__name", "cycle__id", "cycle__project_id") - - return Response( - { - "state_distribution": state_distribution, - "priority_distribution": priority_distribution, - "created_issues": created_issues, - "assigned_issues": assigned_issues_count, - "completed_issues": completed_issues_count, - "pending_issues": pending_issues_count, - "subscribed_issues": subscribed_issues_count, - "present_cycles": present_cycle, - "upcoming_cycles": upcoming_cycles, - } - ) - - -class WorkspaceUserActivityEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceEntityPermission, - ] - - def get(self, request, slug, user_id): - projects = request.query_params.getlist("project", []) - - queryset = IssueActivity.objects.filter( - ~Q(field__in=["comment", "vote", "reaction", "draft"]), - workspace__slug=slug, - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - actor=user_id, - ).select_related("actor", "workspace", "issue", "project") - - if projects: - queryset = queryset.filter(project__in=projects) - - return self.paginate( - request=request, - queryset=queryset, - on_results=lambda issue_activities: IssueActivitySerializer( - issue_activities, many=True - ).data, - ) - - -class ExportWorkspaceUserActivityEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceEntityPermission, - ] - - def generate_csv_from_rows(self, rows): - """Generate CSV buffer from rows.""" - csv_buffer = io.StringIO() - writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL) - [writer.writerow(row) for row in rows] - csv_buffer.seek(0) - return csv_buffer - - def post(self, request, slug, user_id): - - if not request.data.get("date"): - return Response( - {"error": "Date is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - user_activities = IssueActivity.objects.filter( - ~Q(field__in=["comment", "vote", "reaction", "draft"]), - workspace__slug=slug, - created_at__date=request.data.get("date"), - project__project_projectmember__member=request.user, - actor_id=user_id, - ).select_related("actor", "workspace", "issue", "project")[:10000] - - header = [ - "Actor name", - "Issue ID", - "Project", - "Created at", - "Updated at", - "Action", - "Field", - "Old value", - "New value", - ] - rows = [ - ( - activity.actor.display_name, - f"{activity.project.identifier} - {activity.issue.sequence_id if activity.issue else ''}", - activity.project.name, - activity.created_at, - activity.updated_at, - activity.verb, - activity.field, - activity.old_value, - activity.new_value, - ) - for activity in user_activities - ] - csv_buffer = self.generate_csv_from_rows([header] + rows) - response = HttpResponse(csv_buffer.getvalue(), content_type="text/csv") - response["Content-Disposition"] = 'attachment; filename="workspace-user-activity.csv"' - return response - - -class WorkspaceUserProfileEndpoint(BaseAPIView): - def get(self, request, slug, user_id): - user_data = User.objects.get(pk=user_id) - - requesting_workspace_member = WorkspaceMember.objects.get( - workspace__slug=slug, - member=request.user, - is_active=True, - ) - projects = [] - if requesting_workspace_member.role >= 10: - projects = ( - Project.objects.filter( - workspace__slug=slug, - project_projectmember__member=request.user, - project_projectmember__is_active=True, - ) - .annotate( - created_issues=Count( - "project_issue", - filter=Q( - project_issue__created_by_id=user_id, - project_issue__archived_at__isnull=True, - project_issue__is_draft=False, - ), - ) - ) - .annotate( - assigned_issues=Count( - "project_issue", - filter=Q( - project_issue__assignees__in=[user_id], - project_issue__archived_at__isnull=True, - project_issue__is_draft=False, - ), - ) - ) - .annotate( - completed_issues=Count( - "project_issue", - filter=Q( - project_issue__completed_at__isnull=False, - project_issue__assignees__in=[user_id], - project_issue__archived_at__isnull=True, - project_issue__is_draft=False, - ), - ) - ) - .annotate( - pending_issues=Count( - "project_issue", - filter=Q( - project_issue__state__group__in=[ - "backlog", - "unstarted", - "started", - ], - project_issue__assignees__in=[user_id], - project_issue__archived_at__isnull=True, - project_issue__is_draft=False, - ), - ) - ) - .values( - "id", - "created_issues", - "assigned_issues", - "completed_issues", - "pending_issues", - ) - ) - - return Response( - { - "project_data": projects, - "user_data": { - "email": user_data.email, - "first_name": user_data.first_name, - "last_name": user_data.last_name, - "avatar": user_data.avatar, - "cover_image": user_data.cover_image, - "date_joined": user_data.date_joined, - "user_timezone": user_data.user_timezone, - "display_name": user_data.display_name, - }, - }, - status=status.HTTP_200_OK, - ) - - -class WorkspaceUserProfileIssuesEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceViewerPermission, - ] - - def get(self, request, slug, user_id): - fields = [ - field - for field in request.GET.get("fields", "").split(",") - if field - ] - filters = issue_filters(request.query_params, "GET") - - # Custom ordering for priority and state - priority_order = ["urgent", "high", "medium", "low", "none"] - state_order = [ - "backlog", - "unstarted", - "started", - "completed", - "cancelled", - ] - - order_by_param = request.GET.get("order_by", "-created_at") - issue_queryset = ( - Issue.issue_objects.filter( - Q(assignees__in=[user_id]) - | Q(created_by_id=user_id) - | Q(issue_subscribers__subscriber_id=user_id), - workspace__slug=slug, - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - ) - .filter(**filters) - .select_related("workspace", "project", "state", "parent") - .prefetch_related("assignees", "labels", "issue_module__module") - .annotate(cycle_id=F("issue_cycle__cycle_id")) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=~Q(labels__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=~Q(assignees__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "issue_module__module_id", - distinct=True, - filter=~Q(issue_module__module_id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) - .order_by("created_at") - ).distinct() - - # Priority Ordering - if order_by_param == "priority" or order_by_param == "-priority": - priority_order = ( - priority_order - if order_by_param == "priority" - else priority_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - priority_order=Case( - *[ - When(priority=p, then=Value(i)) - for i, p in enumerate(priority_order) - ], - output_field=CharField(), - ) - ).order_by("priority_order") - - # State Ordering - elif order_by_param in [ - "state__name", - "state__group", - "-state__name", - "-state__group", - ]: - state_order = ( - state_order - if order_by_param in ["state__name", "state__group"] - else state_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - state_order=Case( - *[ - When(state__group=state_group, then=Value(i)) - for i, state_group in enumerate(state_order) - ], - default=Value(len(state_order)), - output_field=CharField(), - ) - ).order_by("state_order") - # assignee and label ordering - elif order_by_param in [ - "labels__name", - "-labels__name", - "assignees__first_name", - "-assignees__first_name", - ]: - issue_queryset = issue_queryset.annotate( - max_values=Max( - order_by_param[1::] - if order_by_param.startswith("-") - else order_by_param - ) - ).order_by( - "-max_values" - if order_by_param.startswith("-") - else "max_values" - ) - else: - issue_queryset = issue_queryset.order_by(order_by_param) - - issues = IssueSerializer( - issue_queryset, many=True, fields=fields if fields else None - ).data - return Response(issues, status=status.HTTP_200_OK) - - -class WorkspaceLabelsEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceViewerPermission, - ] - - @cache_response(60 * 60 * 2) - def get(self, request, slug): - labels = Label.objects.filter( - workspace__slug=slug, - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - ) - serializer = LabelSerializer(labels, many=True).data - return Response(serializer, status=status.HTTP_200_OK) - - -class WorkspaceStatesEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceEntityPermission, - ] - - @cache_response(60 * 60 * 2) - def get(self, request, slug): - states = State.objects.filter( - workspace__slug=slug, - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - ) - serializer = StateSerializer(states, many=True).data - return Response(serializer, status=status.HTTP_200_OK) - - -class WorkspaceEstimatesEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceEntityPermission, - ] - - @cache_response(60 * 60 * 2) - def get(self, request, slug): - estimate_ids = Project.objects.filter( - workspace__slug=slug, estimate__isnull=False - ).values_list("estimate_id", flat=True) - estimates = Estimate.objects.filter( - pk__in=estimate_ids - ).prefetch_related( - Prefetch( - "points", - queryset=EstimatePoint.objects.select_related( - "estimate", "workspace", "project" - ), - ) - ) - serializer = WorkspaceEstimateSerializer(estimates, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class WorkspaceModulesEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceViewerPermission, - ] - - def get(self, request, slug): - modules = ( - Module.objects.filter(workspace__slug=slug) - .select_related("project") - .select_related("workspace") - .select_related("lead") - .prefetch_related("members") - .prefetch_related( - Prefetch( - "link_module", - queryset=ModuleLink.objects.select_related( - "module", "created_by" - ), - ) - ) - .annotate( - total_issues=Count( - "issue_module", - filter=Q( - issue_module__issue__archived_at__isnull=True, - issue_module__issue__is_draft=False, - ), - ), - ) - .annotate( - completed_issues=Count( - "issue_module__issue__state__group", - filter=Q( - issue_module__issue__state__group="completed", - issue_module__issue__archived_at__isnull=True, - issue_module__issue__is_draft=False, - ), - ) - ) - .annotate( - cancelled_issues=Count( - "issue_module__issue__state__group", - filter=Q( - issue_module__issue__state__group="cancelled", - issue_module__issue__archived_at__isnull=True, - issue_module__issue__is_draft=False, - ), - ) - ) - .annotate( - started_issues=Count( - "issue_module__issue__state__group", - filter=Q( - issue_module__issue__state__group="started", - issue_module__issue__archived_at__isnull=True, - issue_module__issue__is_draft=False, - ), - ) - ) - .annotate( - unstarted_issues=Count( - "issue_module__issue__state__group", - filter=Q( - issue_module__issue__state__group="unstarted", - issue_module__issue__archived_at__isnull=True, - issue_module__issue__is_draft=False, - ), - ) - ) - .annotate( - backlog_issues=Count( - "issue_module__issue__state__group", - filter=Q( - issue_module__issue__state__group="backlog", - issue_module__issue__archived_at__isnull=True, - issue_module__issue__is_draft=False, - ), - ) - ) - .order_by(self.kwargs.get("order_by", "-created_at")) - ) - - serializer = ModuleSerializer(modules, many=True).data - return Response(serializer, status=status.HTTP_200_OK) - - -class WorkspaceCyclesEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceViewerPermission, - ] - - def get(self, request, slug): - cycles = ( - Cycle.objects.filter(workspace__slug=slug) - .select_related("project") - .select_related("workspace") - .select_related("owned_by") - .annotate( - total_issues=Count( - "issue_cycle", - filter=Q( - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - completed_issues=Count( - "issue_cycle__issue__state__group", - filter=Q( - issue_cycle__issue__state__group="completed", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - cancelled_issues=Count( - "issue_cycle__issue__state__group", - filter=Q( - issue_cycle__issue__state__group="cancelled", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - started_issues=Count( - "issue_cycle__issue__state__group", - filter=Q( - issue_cycle__issue__state__group="started", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - unstarted_issues=Count( - "issue_cycle__issue__state__group", - filter=Q( - issue_cycle__issue__state__group="unstarted", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - backlog_issues=Count( - "issue_cycle__issue__state__group", - filter=Q( - issue_cycle__issue__state__group="backlog", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - total_estimates=Sum("issue_cycle__issue__estimate_point") - ) - .annotate( - completed_estimates=Sum( - "issue_cycle__issue__estimate_point", - filter=Q( - issue_cycle__issue__state__group="completed", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - started_estimates=Sum( - "issue_cycle__issue__estimate_point", - filter=Q( - issue_cycle__issue__state__group="started", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .order_by(self.kwargs.get("order_by", "-created_at")) - .distinct() - ) - serializer = CycleSerializer(cycles, many=True).data - return Response(serializer, status=status.HTTP_200_OK) - - -class WorkspaceUserPropertiesEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceViewerPermission, - ] - - def patch(self, request, slug): - workspace_properties = WorkspaceUserProperties.objects.get( - user=request.user, - workspace__slug=slug, - ) - - workspace_properties.filters = request.data.get( - "filters", workspace_properties.filters - ) - workspace_properties.display_filters = request.data.get( - "display_filters", workspace_properties.display_filters - ) - workspace_properties.display_properties = request.data.get( - "display_properties", workspace_properties.display_properties - ) - workspace_properties.save() - - serializer = WorkspaceUserPropertiesSerializer(workspace_properties) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - def get(self, request, slug): - ( - workspace_properties, - _, - ) = WorkspaceUserProperties.objects.get_or_create( - user=request.user, workspace__slug=slug - ) - serializer = WorkspaceUserPropertiesSerializer(workspace_properties) - return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/workspace/base.py b/apiserver/plane/app/views/workspace/base.py new file mode 100644 index 000000000..0fb8f2d80 --- /dev/null +++ b/apiserver/plane/app/views/workspace/base.py @@ -0,0 +1,414 @@ +# Python imports +from datetime import date +from dateutil.relativedelta import relativedelta +import csv +import io + + +# Django imports +from django.http import HttpResponse +from django.db import IntegrityError +from django.utils import timezone +from django.db.models import ( + Prefetch, + OuterRef, + Func, + F, + Q, + Count, +) +from django.db.models.functions import ExtractWeek, Cast, ExtractDay +from django.db.models.fields import DateField + +# Third party modules +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.serializers import ( + WorkSpaceSerializer, + WorkspaceThemeSerializer, +) +from plane.app.views.base import BaseViewSet, BaseAPIView +from plane.db.models import ( + Workspace, + IssueActivity, + Issue, + WorkspaceTheme, + WorkspaceMember, +) +from plane.app.permissions import ( + WorkSpaceBasePermission, + WorkSpaceAdminPermission, + WorkspaceEntityPermission, +) +from plane.utils.cache import cache_response, invalidate_cache + +class WorkSpaceViewSet(BaseViewSet): + model = Workspace + serializer_class = WorkSpaceSerializer + permission_classes = [ + WorkSpaceBasePermission, + ] + + search_fields = [ + "name", + ] + filterset_fields = [ + "owner", + ] + + lookup_field = "slug" + + def get_queryset(self): + member_count = ( + WorkspaceMember.objects.filter( + workspace=OuterRef("id"), + member__is_bot=False, + is_active=True, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + + issue_count = ( + Issue.issue_objects.filter(workspace=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + return ( + self.filter_queryset( + super().get_queryset().select_related("owner") + ) + .order_by("name") + .filter( + workspace_member__member=self.request.user, + workspace_member__is_active=True, + ) + .annotate(total_members=member_count) + .annotate(total_issues=issue_count) + .select_related("owner") + ) + + @invalidate_cache(path="/api/workspaces/", user=False) + @invalidate_cache(path="/api/users/me/workspaces/") + def create(self, request): + try: + serializer = WorkSpaceSerializer(data=request.data) + + slug = request.data.get("slug", False) + name = request.data.get("name", False) + + if not name or not slug: + return Response( + {"error": "Both name and slug are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if len(name) > 80 or len(slug) > 48: + return Response( + { + "error": "The maximum length for name is 80 and for slug is 48" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + if serializer.is_valid(): + serializer.save(owner=request.user) + # Create Workspace member + _ = WorkspaceMember.objects.create( + workspace_id=serializer.data["id"], + member=request.user, + role=20, + company_role=request.data.get("company_role", ""), + ) + return Response( + serializer.data, status=status.HTTP_201_CREATED + ) + return Response( + [serializer.errors[error][0] for error in serializer.errors], + status=status.HTTP_400_BAD_REQUEST, + ) + + except IntegrityError as e: + if "already exists" in str(e): + return Response( + {"slug": "The workspace with the slug already exists"}, + status=status.HTTP_410_GONE, + ) + @cache_response(60 * 60 * 2) + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) + + @invalidate_cache(path="/api/workspaces/", user=False) + @invalidate_cache(path="/api/users/me/workspaces/") + def partial_update(self, request, *args, **kwargs): + return super().partial_update(request, *args, **kwargs) + + @invalidate_cache(path="/api/workspaces/", user=False) + @invalidate_cache(path="/api/users/me/workspaces/") + def destroy(self, request, *args, **kwargs): + return super().destroy(request, *args, **kwargs) + + +class UserWorkSpacesEndpoint(BaseAPIView): + search_fields = [ + "name", + ] + filterset_fields = [ + "owner", + ] + + @cache_response(60 * 60 * 2) + def get(self, request): + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] + member_count = ( + WorkspaceMember.objects.filter( + workspace=OuterRef("id"), + member__is_bot=False, + is_active=True, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + + issue_count = ( + Issue.issue_objects.filter(workspace=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + + workspace = ( + Workspace.objects.prefetch_related( + Prefetch( + "workspace_member", + queryset=WorkspaceMember.objects.filter( + member=request.user, is_active=True + ), + ) + ) + .select_related("owner") + .annotate(total_members=member_count) + .annotate(total_issues=issue_count) + .filter( + workspace_member__member=request.user, + workspace_member__is_active=True, + ) + .distinct() + ) + workspaces = WorkSpaceSerializer( + self.filter_queryset(workspace), + fields=fields if fields else None, + many=True, + ).data + return Response(workspaces, status=status.HTTP_200_OK) + + +class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView): + def get(self, request): + slug = request.GET.get("slug", False) + + if not slug or slug == "": + return Response( + {"error": "Workspace Slug is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + workspace = Workspace.objects.filter(slug=slug).exists() + return Response({"status": not workspace}, status=status.HTTP_200_OK) + + +class WeekInMonth(Func): + function = "FLOOR" + template = "(((%(expressions)s - 1) / 7) + 1)::INTEGER" + + +class UserWorkspaceDashboardEndpoint(BaseAPIView): + def get(self, request, slug): + issue_activities = ( + IssueActivity.objects.filter( + actor=request.user, + workspace__slug=slug, + created_at__date__gte=date.today() + relativedelta(months=-3), + ) + .annotate(created_date=Cast("created_at", DateField())) + .values("created_date") + .annotate(activity_count=Count("created_date")) + .order_by("created_date") + ) + + month = request.GET.get("month", 1) + + completed_issues = ( + Issue.issue_objects.filter( + assignees__in=[request.user], + workspace__slug=slug, + completed_at__month=month, + completed_at__isnull=False, + ) + .annotate(day_of_month=ExtractDay("completed_at")) + .annotate(week_in_month=WeekInMonth(F("day_of_month"))) + .values("week_in_month") + .annotate(completed_count=Count("id")) + .order_by("week_in_month") + ) + + assigned_issues = Issue.issue_objects.filter( + workspace__slug=slug, assignees__in=[request.user] + ).count() + + pending_issues_count = Issue.issue_objects.filter( + ~Q(state__group__in=["completed", "cancelled"]), + workspace__slug=slug, + assignees__in=[request.user], + ).count() + + completed_issues_count = Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[request.user], + state__group="completed", + ).count() + + issues_due_week = ( + Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[request.user], + ) + .annotate(target_week=ExtractWeek("target_date")) + .filter(target_week=timezone.now().date().isocalendar()[1]) + .count() + ) + + state_distribution = ( + Issue.issue_objects.filter( + workspace__slug=slug, assignees__in=[request.user] + ) + .annotate(state_group=F("state__group")) + .values("state_group") + .annotate(state_count=Count("state_group")) + .order_by("state_group") + ) + + overdue_issues = Issue.issue_objects.filter( + ~Q(state__group__in=["completed", "cancelled"]), + workspace__slug=slug, + assignees__in=[request.user], + target_date__lt=timezone.now(), + completed_at__isnull=True, + ).values("id", "name", "workspace__slug", "project_id", "target_date") + + upcoming_issues = Issue.issue_objects.filter( + ~Q(state__group__in=["completed", "cancelled"]), + start_date__gte=timezone.now(), + workspace__slug=slug, + assignees__in=[request.user], + completed_at__isnull=True, + ).values("id", "name", "workspace__slug", "project_id", "start_date") + + return Response( + { + "issue_activities": issue_activities, + "completed_issues": completed_issues, + "assigned_issues_count": assigned_issues, + "pending_issues_count": pending_issues_count, + "completed_issues_count": completed_issues_count, + "issues_due_week_count": issues_due_week, + "state_distribution": state_distribution, + "overdue_issues": overdue_issues, + "upcoming_issues": upcoming_issues, + }, + status=status.HTTP_200_OK, + ) + + +class WorkspaceThemeViewSet(BaseViewSet): + permission_classes = [ + WorkSpaceAdminPermission, + ] + model = WorkspaceTheme + serializer_class = WorkspaceThemeSerializer + + def get_queryset(self): + return ( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + ) + + def create(self, request, slug): + workspace = Workspace.objects.get(slug=slug) + serializer = WorkspaceThemeSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(workspace=workspace, actor=request.user) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class ExportWorkspaceUserActivityEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceEntityPermission, + ] + + def generate_csv_from_rows(self, rows): + """Generate CSV buffer from rows.""" + csv_buffer = io.StringIO() + writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL) + [writer.writerow(row) for row in rows] + csv_buffer.seek(0) + return csv_buffer + + def post(self, request, slug, user_id): + + if not request.data.get("date"): + return Response( + {"error": "Date is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + user_activities = IssueActivity.objects.filter( + ~Q(field__in=["comment", "vote", "reaction", "draft"]), + workspace__slug=slug, + created_at__date=request.data.get("date"), + project__project_projectmember__member=request.user, + actor_id=user_id, + ).select_related("actor", "workspace", "issue", "project")[:10000] + + header = [ + "Actor name", + "Issue ID", + "Project", + "Created at", + "Updated at", + "Action", + "Field", + "Old value", + "New value", + ] + rows = [ + ( + activity.actor.display_name, + f"{activity.project.identifier} - {activity.issue.sequence_id if activity.issue else ''}", + activity.project.name, + activity.created_at, + activity.updated_at, + activity.verb, + activity.field, + activity.old_value, + activity.new_value, + ) + for activity in user_activities + ] + csv_buffer = self.generate_csv_from_rows([header] + rows) + response = HttpResponse(csv_buffer.getvalue(), content_type="text/csv") + response["Content-Disposition"] = ( + 'attachment; filename="workspace-user-activity.csv"' + ) + return response diff --git a/apiserver/plane/app/views/workspace/cycle.py b/apiserver/plane/app/views/workspace/cycle.py new file mode 100644 index 000000000..ea081cf99 --- /dev/null +++ b/apiserver/plane/app/views/workspace/cycle.py @@ -0,0 +1,116 @@ +# Django imports +from django.db.models import ( + Q, + Count, + Sum, +) + +# Third party modules +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.views.base import BaseAPIView +from plane.db.models import Cycle +from plane.app.permissions import WorkspaceViewerPermission +from plane.app.serializers.cycle import CycleSerializer + + +class WorkspaceCyclesEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceViewerPermission, + ] + + def get(self, request, slug): + cycles = ( + Cycle.objects.filter(workspace__slug=slug) + .select_related("project") + .select_related("workspace") + .select_related("owned_by") + .annotate( + total_issues=Count( + "issue_cycle", + filter=Q( + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + completed_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="completed", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + cancelled_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="cancelled", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + started_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="started", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + unstarted_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="unstarted", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + backlog_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="backlog", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + total_estimates=Sum("issue_cycle__issue__estimate_point") + ) + .annotate( + completed_estimates=Sum( + "issue_cycle__issue__estimate_point", + filter=Q( + issue_cycle__issue__state__group="completed", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + started_estimates=Sum( + "issue_cycle__issue__estimate_point", + filter=Q( + issue_cycle__issue__state__group="started", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .order_by(self.kwargs.get("order_by", "-created_at")) + .distinct() + ) + serializer = CycleSerializer(cycles, many=True).data + return Response(serializer, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/workspace/estimate.py b/apiserver/plane/app/views/workspace/estimate.py new file mode 100644 index 000000000..6b64d8c90 --- /dev/null +++ b/apiserver/plane/app/views/workspace/estimate.py @@ -0,0 +1,39 @@ +# Third party modules +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.serializers import WorkspaceEstimateSerializer +from plane.app.views.base import BaseAPIView +from plane.db.models import Project, Estimate +from plane.app.permissions import WorkspaceEntityPermission + +# Django imports +from django.db.models import ( + Prefetch, +) +from plane.utils.cache import cache_response + + +class WorkspaceEstimatesEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceEntityPermission, + ] + + @cache_response(60 * 60 * 2) + def get(self, request, slug): + estimate_ids = Project.objects.filter( + workspace__slug=slug, estimate__isnull=False + ).values_list("estimate_id", flat=True) + estimates = Estimate.objects.filter( + pk__in=estimate_ids + ).prefetch_related( + Prefetch( + "points", + queryset=Project.objects.select_related( + "estimate", "workspace", "project" + ), + ) + ) + serializer = WorkspaceEstimateSerializer(estimates, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/workspace/invite.py b/apiserver/plane/app/views/workspace/invite.py new file mode 100644 index 000000000..807c060ad --- /dev/null +++ b/apiserver/plane/app/views/workspace/invite.py @@ -0,0 +1,301 @@ +# Python imports +import jwt +from datetime import datetime + +# Django imports +from django.conf import settings +from django.utils import timezone +from django.db.models import Count +from django.core.exceptions import ValidationError +from django.core.validators import validate_email + +# Third party modules +from rest_framework import status +from rest_framework.response import Response +from rest_framework.permissions import AllowAny + +# Module imports +from plane.app.serializers import ( + WorkSpaceMemberSerializer, + WorkSpaceMemberInviteSerializer, +) +from plane.app.views.base import BaseAPIView +from .. import BaseViewSet +from plane.db.models import ( + User, + Workspace, + WorkspaceMemberInvite, + WorkspaceMember, +) +from plane.app.permissions import WorkSpaceAdminPermission +from plane.bgtasks.workspace_invitation_task import workspace_invitation +from plane.bgtasks.event_tracking_task import workspace_invite_event +from plane.utils.cache import invalidate_cache + +class WorkspaceInvitationsViewset(BaseViewSet): + """Endpoint for creating, listing and deleting workspaces""" + + serializer_class = WorkSpaceMemberInviteSerializer + model = WorkspaceMemberInvite + + permission_classes = [ + WorkSpaceAdminPermission, + ] + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .select_related("workspace", "workspace__owner", "created_by") + ) + + def create(self, request, slug): + emails = request.data.get("emails", []) + # Check if email is provided + if not emails: + return Response( + {"error": "Emails are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # check for role level of the requesting user + requesting_user = WorkspaceMember.objects.get( + workspace__slug=slug, + member=request.user, + is_active=True, + ) + + # Check if any invited user has an higher role + if len( + [ + email + for email in emails + if int(email.get("role", 10)) > requesting_user.role + ] + ): + return Response( + {"error": "You cannot invite a user with higher role"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Get the workspace object + workspace = Workspace.objects.get(slug=slug) + + # Check if user is already a member of workspace + workspace_members = WorkspaceMember.objects.filter( + workspace_id=workspace.id, + member__email__in=[email.get("email") for email in emails], + is_active=True, + ).select_related("member", "workspace", "workspace__owner") + + if workspace_members: + return Response( + { + "error": "Some users are already member of workspace", + "workspace_users": WorkSpaceMemberSerializer( + workspace_members, many=True + ).data, + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + workspace_invitations = [] + for email in emails: + try: + validate_email(email.get("email")) + workspace_invitations.append( + WorkspaceMemberInvite( + email=email.get("email").strip().lower(), + workspace_id=workspace.id, + token=jwt.encode( + { + "email": email, + "timestamp": datetime.now().timestamp(), + }, + settings.SECRET_KEY, + algorithm="HS256", + ), + role=email.get("role", 10), + created_by=request.user, + ) + ) + except ValidationError: + return Response( + { + "error": f"Invalid email - {email} provided a valid email address is required to send the invite" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + # Create workspace member invite + workspace_invitations = WorkspaceMemberInvite.objects.bulk_create( + workspace_invitations, batch_size=10, ignore_conflicts=True + ) + + current_site = request.META.get("HTTP_ORIGIN") + + # Send invitations + for invitation in workspace_invitations: + workspace_invitation.delay( + invitation.email, + workspace.id, + invitation.token, + current_site, + request.user.email, + ) + + return Response( + { + "message": "Emails sent successfully", + }, + status=status.HTTP_200_OK, + ) + + def destroy(self, request, slug, pk): + workspace_member_invite = WorkspaceMemberInvite.objects.get( + pk=pk, workspace__slug=slug + ) + workspace_member_invite.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class WorkspaceJoinEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + """Invitation response endpoint the user can respond to the invitation""" + + @invalidate_cache(path="/api/workspaces/", user=False) + @invalidate_cache(path="/api/users/me/workspaces/") + def post(self, request, slug, pk): + workspace_invite = WorkspaceMemberInvite.objects.get( + pk=pk, workspace__slug=slug + ) + + email = request.data.get("email", "") + + # Check the email + if email == "" or workspace_invite.email != email: + return Response( + {"error": "You do not have permission to join the workspace"}, + status=status.HTTP_403_FORBIDDEN, + ) + + # If already responded then return error + if workspace_invite.responded_at is None: + workspace_invite.accepted = request.data.get("accepted", False) + workspace_invite.responded_at = timezone.now() + workspace_invite.save() + + if workspace_invite.accepted: + # Check if the user created account after invitation + user = User.objects.filter(email=email).first() + + # If the user is present then create the workspace member + if user is not None: + # Check if the user was already a member of workspace then activate the user + workspace_member = WorkspaceMember.objects.filter( + workspace=workspace_invite.workspace, member=user + ).first() + if workspace_member is not None: + workspace_member.is_active = True + workspace_member.role = workspace_invite.role + workspace_member.save() + else: + # Create a Workspace + _ = WorkspaceMember.objects.create( + workspace=workspace_invite.workspace, + member=user, + role=workspace_invite.role, + ) + + # Set the user last_workspace_id to the accepted workspace + user.last_workspace_id = workspace_invite.workspace.id + user.save() + + # Delete the invitation + workspace_invite.delete() + + # Send event + workspace_invite_event.delay( + user=user.id if user is not None else None, + email=email, + user_agent=request.META.get("HTTP_USER_AGENT"), + ip=request.META.get("REMOTE_ADDR"), + event_name="MEMBER_ACCEPTED", + accepted_from="EMAIL", + ) + + return Response( + {"message": "Workspace Invitation Accepted"}, + status=status.HTTP_200_OK, + ) + + # Workspace invitation rejected + return Response( + {"message": "Workspace Invitation was not accepted"}, + status=status.HTTP_200_OK, + ) + + return Response( + {"error": "You have already responded to the invitation request"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def get(self, request, slug, pk): + workspace_invitation = WorkspaceMemberInvite.objects.get( + workspace__slug=slug, pk=pk + ) + serializer = WorkSpaceMemberInviteSerializer(workspace_invitation) + return Response(serializer.data, status=status.HTTP_200_OK) + + +class UserWorkspaceInvitationsViewSet(BaseViewSet): + serializer_class = WorkSpaceMemberInviteSerializer + model = WorkspaceMemberInvite + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(email=self.request.user.email) + .select_related("workspace", "workspace__owner", "created_by") + .annotate(total_members=Count("workspace__workspace_member")) + ) + + @invalidate_cache(path="/api/workspaces/", user=False) + @invalidate_cache(path="/api/users/me/workspaces/") + @invalidate_cache( + path="/api/workspaces/:slug/members/", url_params=True, user=False + ) + def create(self, request): + invitations = request.data.get("invitations", []) + workspace_invitations = WorkspaceMemberInvite.objects.filter( + pk__in=invitations, email=request.user.email + ).order_by("-created_at") + + # If the user is already a member of workspace and was deactivated then activate the user + for invitation in workspace_invitations: + # Update the WorkspaceMember for this specific invitation + WorkspaceMember.objects.filter( + workspace_id=invitation.workspace_id, member=request.user + ).update(is_active=True, role=invitation.role) + + # Bulk create the user for all the workspaces + WorkspaceMember.objects.bulk_create( + [ + WorkspaceMember( + workspace=invitation.workspace, + member=request.user, + role=invitation.role, + created_by=request.user, + ) + for invitation in workspace_invitations + ], + ignore_conflicts=True, + ) + + # Delete joined workspace invites + workspace_invitations.delete() + + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/workspace/label.py b/apiserver/plane/app/views/workspace/label.py new file mode 100644 index 000000000..ba396a842 --- /dev/null +++ b/apiserver/plane/app/views/workspace/label.py @@ -0,0 +1,25 @@ +# Third party modules +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.serializers import LabelSerializer +from plane.app.views.base import BaseAPIView +from plane.db.models import Label +from plane.app.permissions import WorkspaceViewerPermission +from plane.utils.cache import cache_response + +class WorkspaceLabelsEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceViewerPermission, + ] + + @cache_response(60 * 60 * 2) + def get(self, request, slug): + labels = Label.objects.filter( + workspace__slug=slug, + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + serializer = LabelSerializer(labels, many=True).data + return Response(serializer, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/workspace/member.py b/apiserver/plane/app/views/workspace/member.py new file mode 100644 index 000000000..ff88e47f8 --- /dev/null +++ b/apiserver/plane/app/views/workspace/member.py @@ -0,0 +1,396 @@ +# Django imports +from django.db.models import ( + Q, + Count, +) +from django.db.models.functions import Cast +from django.db.models import CharField + +# Third party modules +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.serializers import ( + WorkSpaceMemberSerializer, + TeamSerializer, + UserLiteSerializer, + WorkspaceMemberAdminSerializer, + WorkspaceMemberMeSerializer, + ProjectMemberRoleSerializer, +) +from plane.app.views.base import BaseAPIView +from .. import BaseViewSet +from plane.db.models import ( + User, + Workspace, + Team, + ProjectMember, + Project, + WorkspaceMember, +) +from plane.app.permissions import ( + WorkSpaceAdminPermission, + WorkspaceEntityPermission, + WorkspaceUserPermission, +) +from plane.utils.cache import cache_response, invalidate_cache + + +class WorkSpaceMemberViewSet(BaseViewSet): + serializer_class = WorkspaceMemberAdminSerializer + model = WorkspaceMember + + permission_classes = [ + WorkspaceEntityPermission, + ] + + def get_permissions(self): + if self.action == "leave": + self.permission_classes = [ + WorkspaceUserPermission, + ] + else: + self.permission_classes = [ + WorkspaceEntityPermission, + ] + + return super(WorkSpaceMemberViewSet, self).get_permissions() + + search_fields = [ + "member__display_name", + "member__first_name", + ] + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter( + workspace__slug=self.kwargs.get("slug"), + is_active=True, + ) + .select_related("workspace", "workspace__owner") + .select_related("member") + ) + + @cache_response(60 * 60 * 2) + def list(self, request, slug): + workspace_member = WorkspaceMember.objects.get( + member=request.user, + workspace__slug=slug, + is_active=True, + ) + + # Get all active workspace members + workspace_members = self.get_queryset() + + if workspace_member.role > 10: + serializer = WorkspaceMemberAdminSerializer( + workspace_members, + fields=("id", "member", "role"), + many=True, + ) + else: + serializer = WorkSpaceMemberSerializer( + workspace_members, + fields=("id", "member", "role"), + many=True, + ) + return Response(serializer.data, status=status.HTTP_200_OK) + + @invalidate_cache( + path="/api/workspaces/:slug/members/", url_params=True, user=False + ) + def partial_update(self, request, slug, pk): + workspace_member = WorkspaceMember.objects.get( + pk=pk, + workspace__slug=slug, + member__is_bot=False, + is_active=True, + ) + if request.user.id == workspace_member.member_id: + return Response( + {"error": "You cannot update your own role"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Get the requested user role + requested_workspace_member = WorkspaceMember.objects.get( + workspace__slug=slug, + member=request.user, + is_active=True, + ) + # Check if role is being updated + # One cannot update role higher than his own role + if ( + "role" in request.data + and int(request.data.get("role", workspace_member.role)) + > requested_workspace_member.role + ): + return Response( + { + "error": "You cannot update a role that is higher than your own role" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + serializer = WorkSpaceMemberSerializer( + workspace_member, data=request.data, partial=True + ) + + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + @invalidate_cache( + path="/api/workspaces/:slug/members/", url_params=True, user=False + ) + def destroy(self, request, slug, pk): + # Check the user role who is deleting the user + workspace_member = WorkspaceMember.objects.get( + workspace__slug=slug, + pk=pk, + member__is_bot=False, + is_active=True, + ) + + # check requesting user role + requesting_workspace_member = WorkspaceMember.objects.get( + workspace__slug=slug, + member=request.user, + is_active=True, + ) + + if str(workspace_member.id) == str(requesting_workspace_member.id): + return Response( + { + "error": "You cannot remove yourself from the workspace. Please use leave workspace" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + if requesting_workspace_member.role < workspace_member.role: + return Response( + { + "error": "You cannot remove a user having role higher than you" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + if ( + Project.objects.annotate( + total_members=Count("project_projectmember"), + member_with_role=Count( + "project_projectmember", + filter=Q( + project_projectmember__member_id=workspace_member.id, + project_projectmember__role=20, + ), + ), + ) + .filter(total_members=1, member_with_role=1, workspace__slug=slug) + .exists() + ): + return Response( + { + "error": "User is a part of some projects where they are the only admin, they should either leave that project or promote another user to admin." + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Deactivate the users from the projects where the user is part of + _ = ProjectMember.objects.filter( + workspace__slug=slug, + member_id=workspace_member.member_id, + is_active=True, + ).update(is_active=False) + + workspace_member.is_active = False + workspace_member.save() + return Response(status=status.HTTP_204_NO_CONTENT) + + @invalidate_cache( + path="/api/workspaces/:slug/members/", url_params=True, user=False + ) + def leave(self, request, slug): + workspace_member = WorkspaceMember.objects.get( + workspace__slug=slug, + member=request.user, + is_active=True, + ) + + # Check if the leaving user is the only admin of the workspace + if ( + workspace_member.role == 20 + and not WorkspaceMember.objects.filter( + workspace__slug=slug, + role=20, + is_active=True, + ).count() + > 1 + ): + return Response( + { + "error": "You cannot leave the workspace as you are the only admin of the workspace you will have to either delete the workspace or promote another user to admin." + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + if ( + Project.objects.annotate( + total_members=Count("project_projectmember"), + member_with_role=Count( + "project_projectmember", + filter=Q( + project_projectmember__member_id=request.user.id, + project_projectmember__role=20, + ), + ), + ) + .filter(total_members=1, member_with_role=1, workspace__slug=slug) + .exists() + ): + return Response( + { + "error": "You are a part of some projects where you are the only admin, you should either leave the project or promote another user to admin." + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + # # Deactivate the users from the projects where the user is part of + _ = ProjectMember.objects.filter( + workspace__slug=slug, + member_id=workspace_member.member_id, + is_active=True, + ).update(is_active=False) + + # # Deactivate the user + workspace_member.is_active = False + workspace_member.save() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class WorkspaceMemberUserViewsEndpoint(BaseAPIView): + def post(self, request, slug): + workspace_member = WorkspaceMember.objects.get( + workspace__slug=slug, + member=request.user, + is_active=True, + ) + workspace_member.view_props = request.data.get("view_props", {}) + workspace_member.save() + + return Response(status=status.HTTP_204_NO_CONTENT) + + +class WorkspaceMemberUserEndpoint(BaseAPIView): + def get(self, request, slug): + workspace_member = WorkspaceMember.objects.get( + member=request.user, + workspace__slug=slug, + is_active=True, + ) + serializer = WorkspaceMemberMeSerializer(workspace_member) + return Response(serializer.data, status=status.HTTP_200_OK) + + +class WorkspaceProjectMemberEndpoint(BaseAPIView): + serializer_class = ProjectMemberRoleSerializer + model = ProjectMember + + permission_classes = [ + WorkspaceEntityPermission, + ] + + def get(self, request, slug): + # Fetch all project IDs where the user is involved + project_ids = ( + ProjectMember.objects.filter( + member=request.user, + is_active=True, + ) + .values_list("project_id", flat=True) + .distinct() + ) + + # Get all the project members in which the user is involved + project_members = ProjectMember.objects.filter( + workspace__slug=slug, + project_id__in=project_ids, + is_active=True, + ).select_related("project", "member", "workspace") + project_members = ProjectMemberRoleSerializer( + project_members, many=True + ).data + + project_members_dict = dict() + + # Construct a dictionary with project_id as key and project_members as value + for project_member in project_members: + project_id = project_member.pop("project") + if str(project_id) not in project_members_dict: + project_members_dict[str(project_id)] = [] + project_members_dict[str(project_id)].append(project_member) + + return Response(project_members_dict, status=status.HTTP_200_OK) + + +class TeamMemberViewSet(BaseViewSet): + serializer_class = TeamSerializer + model = Team + permission_classes = [ + WorkSpaceAdminPermission, + ] + + search_fields = [ + "member__display_name", + "member__first_name", + ] + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .select_related("workspace", "workspace__owner") + .prefetch_related("members") + ) + + def create(self, request, slug): + members = list( + WorkspaceMember.objects.filter( + workspace__slug=slug, + member__id__in=request.data.get("members", []), + is_active=True, + ) + .annotate(member_str_id=Cast("member", output_field=CharField())) + .distinct() + .values_list("member_str_id", flat=True) + ) + + if len(members) != len(request.data.get("members", [])): + users = list( + set(request.data.get("members", [])).difference(members) + ) + users = User.objects.filter(pk__in=users) + + serializer = UserLiteSerializer(users, many=True) + return Response( + { + "error": f"{len(users)} of the member(s) are not a part of the workspace", + "members": serializer.data, + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + workspace = Workspace.objects.get(slug=slug) + + serializer = TeamSerializer( + data=request.data, context={"workspace": workspace} + ) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/apiserver/plane/app/views/workspace/module.py b/apiserver/plane/app/views/workspace/module.py new file mode 100644 index 000000000..fbd760271 --- /dev/null +++ b/apiserver/plane/app/views/workspace/module.py @@ -0,0 +1,104 @@ +# Django imports +from django.db.models import ( + Prefetch, + Q, + Count, +) + +# Third party modules +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.views.base import BaseAPIView +from plane.db.models import ( + Module, + ModuleLink, +) +from plane.app.permissions import WorkspaceViewerPermission +from plane.app.serializers.module import ModuleSerializer + +class WorkspaceModulesEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceViewerPermission, + ] + + def get(self, request, slug): + modules = ( + Module.objects.filter(workspace__slug=slug) + .select_related("project") + .select_related("workspace") + .select_related("lead") + .prefetch_related("members") + .prefetch_related( + Prefetch( + "link_module", + queryset=ModuleLink.objects.select_related( + "module", "created_by" + ), + ) + ) + .annotate( + total_issues=Count( + "issue_module", + filter=Q( + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), + ), + ) + .annotate( + completed_issues=Count( + "issue_module__issue__state__group", + filter=Q( + issue_module__issue__state__group="completed", + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), + ) + ) + .annotate( + cancelled_issues=Count( + "issue_module__issue__state__group", + filter=Q( + issue_module__issue__state__group="cancelled", + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), + ) + ) + .annotate( + started_issues=Count( + "issue_module__issue__state__group", + filter=Q( + issue_module__issue__state__group="started", + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), + ) + ) + .annotate( + unstarted_issues=Count( + "issue_module__issue__state__group", + filter=Q( + issue_module__issue__state__group="unstarted", + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), + ) + ) + .annotate( + backlog_issues=Count( + "issue_module__issue__state__group", + filter=Q( + issue_module__issue__state__group="backlog", + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), + ) + ) + .order_by(self.kwargs.get("order_by", "-created_at")) + ) + + serializer = ModuleSerializer(modules, many=True).data + return Response(serializer, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/workspace/state.py b/apiserver/plane/app/views/workspace/state.py new file mode 100644 index 000000000..d44f83e73 --- /dev/null +++ b/apiserver/plane/app/views/workspace/state.py @@ -0,0 +1,25 @@ +# Third party modules +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.serializers import StateSerializer +from plane.app.views.base import BaseAPIView +from plane.db.models import State +from plane.app.permissions import WorkspaceEntityPermission +from plane.utils.cache import cache_response + +class WorkspaceStatesEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceEntityPermission, + ] + + @cache_response(60 * 60 * 2) + def get(self, request, slug): + states = State.objects.filter( + workspace__slug=slug, + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + serializer = StateSerializer(states, many=True).data + return Response(serializer, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/workspace/user.py b/apiserver/plane/app/views/workspace/user.py new file mode 100644 index 000000000..36b00b738 --- /dev/null +++ b/apiserver/plane/app/views/workspace/user.py @@ -0,0 +1,573 @@ +# Python imports +from datetime import date +from dateutil.relativedelta import relativedelta + +# Django imports +from django.utils import timezone +from django.db.models import ( + OuterRef, + Func, + F, + Q, + Count, + Case, + Value, + CharField, + When, + Max, + IntegerField, + UUIDField, +) +from django.db.models.functions import ExtractWeek, Cast +from django.db.models.fields import DateField +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.db.models.functions import Coalesce + +# Third party modules +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.serializers import ( + WorkSpaceSerializer, + ProjectMemberSerializer, + IssueActivitySerializer, + IssueSerializer, + WorkspaceUserPropertiesSerializer, +) +from plane.app.views.base import BaseAPIView +from plane.db.models import ( + User, + Workspace, + ProjectMember, + IssueActivity, + Issue, + IssueLink, + IssueAttachment, + IssueSubscriber, + Project, + WorkspaceMember, + CycleIssue, + WorkspaceUserProperties, +) +from plane.app.permissions import ( + WorkspaceEntityPermission, + WorkspaceViewerPermission, +) +from plane.utils.issue_filters import issue_filters + + +class UserLastProjectWithWorkspaceEndpoint(BaseAPIView): + def get(self, request): + user = User.objects.get(pk=request.user.id) + + last_workspace_id = user.last_workspace_id + + if last_workspace_id is None: + return Response( + { + "project_details": [], + "workspace_details": {}, + }, + status=status.HTTP_200_OK, + ) + + workspace = Workspace.objects.get(pk=last_workspace_id) + workspace_serializer = WorkSpaceSerializer(workspace) + + project_member = ProjectMember.objects.filter( + workspace_id=last_workspace_id, member=request.user + ).select_related("workspace", "project", "member", "workspace__owner") + + project_member_serializer = ProjectMemberSerializer( + project_member, many=True + ) + + return Response( + { + "workspace_details": workspace_serializer.data, + "project_details": project_member_serializer.data, + }, + status=status.HTTP_200_OK, + ) + + +class WorkspaceUserProfileIssuesEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceViewerPermission, + ] + + def get(self, request, slug, user_id): + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] + filters = issue_filters(request.query_params, "GET") + + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", "none"] + state_order = [ + "backlog", + "unstarted", + "started", + "completed", + "cancelled", + ] + + order_by_param = request.GET.get("order_by", "-created_at") + issue_queryset = ( + Issue.issue_objects.filter( + Q(assignees__in=[user_id]) + | Q(created_by_id=user_id) + | Q(issue_subscribers__subscriber_id=user_id), + workspace__slug=slug, + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True + ) + .filter(**filters) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels", "issue_module__module") + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + .order_by("created_at") + ).distinct() + + # Priority Ordering + if order_by_param == "priority" or order_by_param == "-priority": + priority_order = ( + priority_order + if order_by_param == "priority" + else priority_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), + ) + ).order_by("priority_order") + + # State Ordering + elif order_by_param in [ + "state__name", + "state__group", + "-state__name", + "-state__group", + ]: + state_order = ( + state_order + if order_by_param in ["state__name", "state__group"] + else state_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + state_order=Case( + *[ + When(state__group=state_group, then=Value(i)) + for i, state_group in enumerate(state_order) + ], + default=Value(len(state_order)), + output_field=CharField(), + ) + ).order_by("state_order") + # assignee and label ordering + elif order_by_param in [ + "labels__name", + "-labels__name", + "assignees__first_name", + "-assignees__first_name", + ]: + issue_queryset = issue_queryset.annotate( + max_values=Max( + order_by_param[1::] + if order_by_param.startswith("-") + else order_by_param + ) + ).order_by( + "-max_values" + if order_by_param.startswith("-") + else "max_values" + ) + else: + issue_queryset = issue_queryset.order_by(order_by_param) + + issues = IssueSerializer( + issue_queryset, many=True, fields=fields if fields else None + ).data + return Response(issues, status=status.HTTP_200_OK) + + +class WorkspaceUserPropertiesEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceViewerPermission, + ] + + def patch(self, request, slug): + workspace_properties = WorkspaceUserProperties.objects.get( + user=request.user, + workspace__slug=slug, + ) + + workspace_properties.filters = request.data.get( + "filters", workspace_properties.filters + ) + workspace_properties.display_filters = request.data.get( + "display_filters", workspace_properties.display_filters + ) + workspace_properties.display_properties = request.data.get( + "display_properties", workspace_properties.display_properties + ) + workspace_properties.save() + + serializer = WorkspaceUserPropertiesSerializer(workspace_properties) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def get(self, request, slug): + ( + workspace_properties, + _, + ) = WorkspaceUserProperties.objects.get_or_create( + user=request.user, workspace__slug=slug + ) + serializer = WorkspaceUserPropertiesSerializer(workspace_properties) + return Response(serializer.data, status=status.HTTP_200_OK) + + +class WorkspaceUserProfileEndpoint(BaseAPIView): + def get(self, request, slug, user_id): + user_data = User.objects.get(pk=user_id) + + requesting_workspace_member = WorkspaceMember.objects.get( + workspace__slug=slug, + member=request.user, + is_active=True, + ) + projects = [] + if requesting_workspace_member.role >= 10: + projects = ( + Project.objects.filter( + workspace__slug=slug, + project_projectmember__member=request.user, + project_projectmember__is_active=True, + ) + .annotate( + created_issues=Count( + "project_issue", + filter=Q( + project_issue__created_by_id=user_id, + project_issue__archived_at__isnull=True, + project_issue__is_draft=False, + ), + ) + ) + .annotate( + assigned_issues=Count( + "project_issue", + filter=Q( + project_issue__assignees__in=[user_id], + project_issue__archived_at__isnull=True, + project_issue__is_draft=False, + ), + ) + ) + .annotate( + completed_issues=Count( + "project_issue", + filter=Q( + project_issue__completed_at__isnull=False, + project_issue__assignees__in=[user_id], + project_issue__archived_at__isnull=True, + project_issue__is_draft=False, + ), + ) + ) + .annotate( + pending_issues=Count( + "project_issue", + filter=Q( + project_issue__state__group__in=[ + "backlog", + "unstarted", + "started", + ], + project_issue__assignees__in=[user_id], + project_issue__archived_at__isnull=True, + project_issue__is_draft=False, + ), + ) + ) + .values( + "id", + "logo_props", + "created_issues", + "assigned_issues", + "completed_issues", + "pending_issues", + ) + ) + + return Response( + { + "project_data": projects, + "user_data": { + "email": user_data.email, + "first_name": user_data.first_name, + "last_name": user_data.last_name, + "avatar": user_data.avatar, + "cover_image": user_data.cover_image, + "date_joined": user_data.date_joined, + "user_timezone": user_data.user_timezone, + "display_name": user_data.display_name, + }, + }, + status=status.HTTP_200_OK, + ) + + +class WorkspaceUserActivityEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceEntityPermission, + ] + + def get(self, request, slug, user_id): + projects = request.query_params.getlist("project", []) + + queryset = IssueActivity.objects.filter( + ~Q(field__in=["comment", "vote", "reaction", "draft"]), + workspace__slug=slug, + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + actor=user_id, + ).select_related("actor", "workspace", "issue", "project") + + if projects: + queryset = queryset.filter(project__in=projects) + + return self.paginate( + request=request, + queryset=queryset, + on_results=lambda issue_activities: IssueActivitySerializer( + issue_activities, many=True + ).data, + ) + + +class WorkspaceUserProfileStatsEndpoint(BaseAPIView): + def get(self, request, slug, user_id): + filters = issue_filters(request.query_params, "GET") + + state_distribution = ( + Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[user_id], + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + .filter(**filters) + .annotate(state_group=F("state__group")) + .values("state_group") + .annotate(state_count=Count("state_group")) + .order_by("state_group") + ) + + priority_order = ["urgent", "high", "medium", "low", "none"] + + priority_distribution = ( + Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[user_id], + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + .filter(**filters) + .values("priority") + .annotate(priority_count=Count("priority")) + .filter(priority_count__gte=1) + .annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + default=Value(len(priority_order)), + output_field=IntegerField(), + ) + ) + .order_by("priority_order") + ) + + created_issues = ( + Issue.issue_objects.filter( + workspace__slug=slug, + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + created_by_id=user_id, + ) + .filter(**filters) + .count() + ) + + assigned_issues_count = ( + Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[user_id], + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + .filter(**filters) + .count() + ) + + pending_issues_count = ( + Issue.issue_objects.filter( + ~Q(state__group__in=["completed", "cancelled"]), + workspace__slug=slug, + assignees__in=[user_id], + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + .filter(**filters) + .count() + ) + + completed_issues_count = ( + Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[user_id], + state__group="completed", + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + .filter(**filters) + .count() + ) + + subscribed_issues_count = ( + IssueSubscriber.objects.filter( + workspace__slug=slug, + subscriber_id=user_id, + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + .filter(**filters) + .count() + ) + + upcoming_cycles = CycleIssue.objects.filter( + workspace__slug=slug, + cycle__start_date__gt=timezone.now().date(), + issue__assignees__in=[ + user_id, + ], + ).values("cycle__name", "cycle__id", "cycle__project_id") + + present_cycle = CycleIssue.objects.filter( + workspace__slug=slug, + cycle__start_date__lt=timezone.now().date(), + cycle__end_date__gt=timezone.now().date(), + issue__assignees__in=[ + user_id, + ], + ).values("cycle__name", "cycle__id", "cycle__project_id") + + return Response( + { + "state_distribution": state_distribution, + "priority_distribution": priority_distribution, + "created_issues": created_issues, + "assigned_issues": assigned_issues_count, + "completed_issues": completed_issues_count, + "pending_issues": pending_issues_count, + "subscribed_issues": subscribed_issues_count, + "present_cycles": present_cycle, + "upcoming_cycles": upcoming_cycles, + } + ) + + +class UserActivityGraphEndpoint(BaseAPIView): + def get(self, request, slug): + issue_activities = ( + IssueActivity.objects.filter( + actor=request.user, + workspace__slug=slug, + created_at__date__gte=date.today() + relativedelta(months=-6), + ) + .annotate(created_date=Cast("created_at", DateField())) + .values("created_date") + .annotate(activity_count=Count("created_date")) + .order_by("created_date") + ) + + return Response(issue_activities, status=status.HTTP_200_OK) + + +class UserIssueCompletedGraphEndpoint(BaseAPIView): + def get(self, request, slug): + month = request.GET.get("month", 1) + + issues = ( + Issue.issue_objects.filter( + assignees__in=[request.user], + workspace__slug=slug, + completed_at__month=month, + completed_at__isnull=False, + ) + .annotate(completed_week=ExtractWeek("completed_at")) + .annotate(week=F("completed_week") % 4) + .values("week") + .annotate(completed_count=Count("completed_week")) + .order_by("week") + ) + + return Response(issues, status=status.HTTP_200_OK) From 7a8aef45993df4c6de60e34c98eb981da7c5befb Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Thu, 7 Mar 2024 15:56:02 +0530 Subject: [PATCH 065/308] Fix: rendering issue in kanban swimlanes when the cycle or module is not assigned to an issue (#3899) --- web/store/issue/helpers/issue-helper.store.ts | 56 ++++++++++--------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/web/store/issue/helpers/issue-helper.store.ts b/web/store/issue/helpers/issue-helper.store.ts index 50e04e890..cc86560bd 100644 --- a/web/store/issue/helpers/issue-helper.store.ts +++ b/web/store/issue/helpers/issue-helper.store.ts @@ -62,35 +62,35 @@ export class IssueHelperStore implements TIssueHelperStore { issues: TIssueMap, isCalendarIssues: boolean = false ) => { - const _issues: { [group_id: string]: string[] } = {}; - if (!groupBy) return _issues; + const currentIssues: { [group_id: string]: string[] } = {}; + if (!groupBy) return currentIssues; this.issueDisplayFiltersDefaultData(groupBy).forEach((group) => { - _issues[group] = []; + currentIssues[group] = []; }); const projectIssues = this.issuesSortWithOrderBy(issues, orderBy); for (const issue in projectIssues) { - const _issue = projectIssues[issue]; + const currentIssue = projectIssues[issue]; let groupArray = []; if (groupBy === "state_detail.group") { // if groupBy state_detail.group is coming from the project level the we are using stateDetails from root store else we are looping through the stateMap - const state_group = (this.rootStore?.stateMap || {})?.[_issue?.state_id]?.group || "None"; + const state_group = (this.rootStore?.stateMap || {})?.[currentIssue?.state_id]?.group || "None"; groupArray = [state_group]; } else { - const groupValue = get(_issue, ISSUE_FILTER_DEFAULT_DATA[groupBy]); - groupArray = groupValue !== undefined ? this.getGroupArray(groupValue, isCalendarIssues) : []; + const groupValue = get(currentIssue, ISSUE_FILTER_DEFAULT_DATA[groupBy]); + groupArray = groupValue !== undefined ? this.getGroupArray(groupValue, isCalendarIssues) : ["None"]; } for (const group of groupArray) { - if (group && _issues[group]) _issues[group].push(_issue.id); - else if (group) _issues[group] = [_issue.id]; + if (group && currentIssues[group]) currentIssues[group].push(currentIssue.id); + else if (group) currentIssues[group] = [currentIssue.id]; } } - return _issues; + return currentIssues; }; subGroupedIssues = ( @@ -99,45 +99,47 @@ export class IssueHelperStore implements TIssueHelperStore { orderBy: TIssueOrderByOptions, issues: TIssueMap ) => { - const _issues: { [sub_group_id: string]: { [group_id: string]: string[] } } = {}; - if (!subGroupBy || !groupBy) return _issues; + const currentIssues: { [sub_group_id: string]: { [group_id: string]: string[] } } = {}; + if (!subGroupBy || !groupBy) return currentIssues; - this.issueDisplayFiltersDefaultData(subGroupBy).forEach((sub_group: any) => { + this.issueDisplayFiltersDefaultData(subGroupBy).forEach((sub_group) => { const groupByIssues: { [group_id: string]: string[] } = {}; this.issueDisplayFiltersDefaultData(groupBy).forEach((group) => { groupByIssues[group] = []; }); - _issues[sub_group] = groupByIssues; + currentIssues[sub_group] = groupByIssues; }); const projectIssues = this.issuesSortWithOrderBy(issues, orderBy); for (const issue in projectIssues) { - const _issue = projectIssues[issue]; + const currentIssue = projectIssues[issue]; let subGroupArray = []; let groupArray = []; if (subGroupBy === "state_detail.group" || groupBy === "state_detail.group") { - const state_group = (this.rootStore?.stateMap || {})?.[_issue?.state_id]?.group || "None"; + const state_group = (this.rootStore?.stateMap || {})?.[currentIssue?.state_id]?.group || "None"; subGroupArray = [state_group]; groupArray = [state_group]; } else { - const subGroupValue = get(_issue, ISSUE_FILTER_DEFAULT_DATA[subGroupBy]); - const groupValue = get(_issue, ISSUE_FILTER_DEFAULT_DATA[groupBy]); - subGroupArray = subGroupValue != undefined ? this.getGroupArray(subGroupValue) : []; - groupArray = groupValue != undefined ? this.getGroupArray(groupValue) : []; + const subGroupValue = get(currentIssue, ISSUE_FILTER_DEFAULT_DATA[subGroupBy]); + const groupValue = get(currentIssue, ISSUE_FILTER_DEFAULT_DATA[groupBy]); + + subGroupArray = subGroupValue != undefined ? this.getGroupArray(subGroupValue) : ["None"]; + groupArray = groupValue != undefined ? this.getGroupArray(groupValue) : ["None"]; } for (const subGroup of subGroupArray) { for (const group of groupArray) { - if (subGroup && group && _issues?.[subGroup]?.[group]) _issues[subGroup][group].push(_issue.id); - else if (subGroup && group && _issues[subGroup]) _issues[subGroup][group] = [_issue.id]; - else if (subGroup && group) _issues[subGroup] = { [group]: [_issue.id] }; + if (subGroup && group && currentIssues?.[subGroup]?.[group]) + currentIssues[subGroup][group].push(currentIssue.id); + else if (subGroup && group && currentIssues[subGroup]) currentIssues[subGroup][group] = [currentIssue.id]; + else if (subGroup && group) currentIssues[subGroup] = { [group]: [currentIssue.id] }; } } } - return _issues; + return currentIssues; }; unGroupedIssues = (orderBy: TIssueOrderByOptions, issues: TIssueMap) => @@ -215,8 +217,8 @@ export class IssueHelperStore implements TIssueHelperStore { const moduleMap = this.rootStore?.moduleMap; if (!moduleMap) break; for (const dataId of dataIdsArray) { - const _module = moduleMap[dataId]; - if (_module && _module.name) dataValues.push(_module.name.toLocaleLowerCase()); + const currentModule = moduleMap[dataId]; + if (currentModule && currentModule.name) dataValues.push(currentModule.name.toLocaleLowerCase()); } break; case "cycle_id": @@ -388,7 +390,7 @@ export class IssueHelperStore implements TIssueHelperStore { getGroupArray(value: boolean | number | string | string[] | null, isDate: boolean = false): string[] { if (!value || value === null || value === undefined) return ["None"]; if (Array.isArray(value)) - if (value.length) return value; + if (value && value.length) return value; else return ["None"]; else if (typeof value === "boolean") return [value ? "True" : "False"]; else if (typeof value === "number") return [value.toString()]; From 47a7f60611239f0b58b8b340d7ba6af15b97aaf4 Mon Sep 17 00:00:00 2001 From: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com> Date: Thu, 7 Mar 2024 16:53:43 +0530 Subject: [PATCH 066/308] [WEB-667] fix: unable to deselect project lead in create project modal (#3898) * fix: unable to deselect project lead in create project modal * removed unneccessary code --- web/components/project/create-project-modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/project/create-project-modal.tsx b/web/components/project/create-project-modal.tsx index 4d30c3dec..b8e59f032 100644 --- a/web/components/project/create-project-modal.tsx +++ b/web/components/project/create-project-modal.tsx @@ -406,7 +406,7 @@ export const CreateProjectModal: FC = observer((props) => {
onChange(lead === value ? null : lead)} placeholder="Lead" multiple={false} buttonVariant="border-with-text" From 7b88a2a88cc777be716a3afdd80bc82ea791bad5 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Thu, 7 Mar 2024 16:53:59 +0530 Subject: [PATCH 067/308] [WEB-469] chore: selected filter sorting added in filter dropdown (#3869) * chore: selected filter sorting added in filter dropdown * chore: handleClearAllFilters function updated * chore: filter dropdown sorting updated * chore: filter dropdown sorting updated * chore: filter dropdown sorting updated --------- Co-authored-by: gurusainath --- .../empty-states/archived-issues.tsx | 2 +- .../empty-states/draft-issues.tsx | 2 +- .../empty-states/project-issues.tsx | 2 +- .../applied-filters/roots/archived-issue.tsx | 2 +- .../applied-filters/roots/cycle-root.tsx | 2 +- .../applied-filters/roots/draft-issue.tsx | 2 +- .../roots/global-view-root.tsx | 2 +- .../applied-filters/roots/module-root.tsx | 2 +- .../roots/profile-issues-root.tsx | 2 +- .../applied-filters/roots/project-root.tsx | 2 +- .../roots/project-view-root.tsx | 2 +- .../filters/header/filters/assignee.tsx | 37 ++++++++++++------- .../filters/header/filters/created-by.tsx | 34 +++++++++++------ .../filters/header/filters/cycle.tsx | 34 ++++++++++------- .../filters/header/filters/labels.tsx | 31 +++++++++++----- .../filters/header/filters/mentions.tsx | 33 +++++++++++------ .../filters/header/filters/module.tsx | 34 ++++++++++------- .../filters/header/filters/project.tsx | 31 +++++++++++----- .../filters/header/filters/state-group.tsx | 2 +- .../filters/header/filters/state.tsx | 26 ++++++++----- .../issue-layouts/roots/cycle-layout-root.tsx | 2 +- .../roots/module-layout-root.tsx | 2 +- 22 files changed, 180 insertions(+), 108 deletions(-) diff --git a/web/components/issues/issue-layouts/empty-states/archived-issues.tsx b/web/components/issues/issue-layouts/empty-states/archived-issues.tsx index c9de2279c..36c895de3 100644 --- a/web/components/issues/issue-layouts/empty-states/archived-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/archived-issues.tsx @@ -32,7 +32,7 @@ export const ProjectArchivedEmptyState: React.FC = observer(() => { if (!workspaceSlug || !projectId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); issuesFilter.updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { ...newFilters, diff --git a/web/components/issues/issue-layouts/empty-states/draft-issues.tsx b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx index 0968ed07a..c23fea100 100644 --- a/web/components/issues/issue-layouts/empty-states/draft-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx @@ -31,7 +31,7 @@ export const ProjectDraftEmptyState: React.FC = observer(() => { if (!workspaceSlug || !projectId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); issuesFilter.updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { ...newFilters, diff --git a/web/components/issues/issue-layouts/empty-states/project-issues.tsx b/web/components/issues/issue-layouts/empty-states/project-issues.tsx index 12642d364..58929e48d 100644 --- a/web/components/issues/issue-layouts/empty-states/project-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/project-issues.tsx @@ -34,7 +34,7 @@ export const ProjectEmptyState: React.FC = observer(() => { if (!workspaceSlug || !projectId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); issuesFilter.updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { ...newFilters, diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx index 35651d870..7e6926fa5 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx @@ -56,7 +56,7 @@ export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => { const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx index 6a741b73d..ee6b4e694 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx @@ -62,7 +62,7 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => { if (!workspaceSlug || !projectId || !cycleId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); updateFilters( workspaceSlug.toString(), diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx index a075d59d2..61c0e346a 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx @@ -53,7 +53,7 @@ export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => { const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { ...newFilters }); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx index a431652f1..d907cf168 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx @@ -76,7 +76,7 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => { if (!workspaceSlug || !globalViewId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); updateFilters( workspaceSlug.toString(), diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx index b49ddf4d6..fc78d79ea 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx @@ -61,7 +61,7 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => { if (!workspaceSlug || !projectId || !moduleId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); updateFilters( workspaceSlug.toString(), diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx index 91eeef423..b0c496a7b 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx @@ -57,7 +57,7 @@ export const ProfileIssuesAppliedFiltersRoot: React.FC = observer(() => { if (!workspaceSlug || !userId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); updateFilters(workspaceSlug.toString(), undefined, EIssueFilterType.FILTERS, { ...newFilters }, userId.toString()); }; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx index c0b67043a..602f3fa2d 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx @@ -59,7 +59,7 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => { if (!workspaceSlug || !projectId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { ...newFilters }); }; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx index 760d2e7e4..6586159fa 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx @@ -67,7 +67,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { if (!workspaceSlug || !projectId || !viewId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); updateFilters( workspaceSlug.toString(), diff --git a/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx b/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx index b26b688af..c51fcf7ab 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx @@ -1,11 +1,12 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { observer } from "mobx-react-lite"; +import sortBy from "lodash/sortBy"; // hooks -import { Avatar, Loader } from "@plane/ui"; -import { FilterHeader, FilterOption } from "components/issues"; import { useMember } from "hooks/store"; // components +import { FilterHeader, FilterOption } from "components/issues"; // ui +import { Avatar, Loader } from "@plane/ui"; type Props = { appliedFilters: string[] | null; @@ -24,15 +25,23 @@ export const FilterAssignees: React.FC = observer((props: Props) => { const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = memberIds?.filter( - (memberId) => getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) - ); + const sortedOptions = useMemo(() => { + const filteredOptions = (memberIds || []).filter((memberId) => + getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return sortBy(filteredOptions, [ + (memberId) => !(appliedFilters ?? []).includes(memberId), + (memberId) => getUserDetails(memberId)?.display_name.toLowerCase(), + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); const handleViewToggle = () => { - if (!filteredOptions) return; + if (!sortedOptions) return; - if (itemsToRender === filteredOptions.length) setItemsToRender(5); - else setItemsToRender(filteredOptions.length); + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); }; return ( @@ -44,10 +53,10 @@ export const FilterAssignees: React.FC = observer((props: Props) => { /> {previewEnabled && (
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( + {sortedOptions ? ( + sortedOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((memberId) => { + {sortedOptions.slice(0, itemsToRender).map((memberId) => { const member = getUserDetails(memberId); if (!member) return null; @@ -61,13 +70,13 @@ export const FilterAssignees: React.FC = observer((props: Props) => { /> ); })} - {filteredOptions.length > 5 && ( + {sortedOptions.length > 5 && ( )} diff --git a/web/components/issues/issue-layouts/filters/header/filters/created-by.tsx b/web/components/issues/issue-layouts/filters/header/filters/created-by.tsx index 45e3309a9..765955bf9 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/created-by.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/created-by.tsx @@ -1,5 +1,6 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { observer } from "mobx-react-lite"; +import sortBy from "lodash/sortBy"; // hooks import { Avatar, Loader } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; @@ -22,16 +23,25 @@ export const FilterCreatedBy: React.FC = observer((props: Props) => { // store hooks const { getUserDetails } = useMember(); - const filteredOptions = memberIds?.filter( - (memberId) => getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) - ); + const sortedOptions = useMemo(() => { + const filteredOptions = (memberIds || []).filter((memberId) => + getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return sortBy(filteredOptions, [ + (memberId) => !(appliedFilters ?? []).includes(memberId), + (memberId) => getUserDetails(memberId)?.display_name.toLowerCase(), + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); + const appliedFiltersCount = appliedFilters?.length ?? 0; const handleViewToggle = () => { - if (!filteredOptions) return; + if (!sortedOptions) return; - if (itemsToRender === filteredOptions.length) setItemsToRender(5); - else setItemsToRender(filteredOptions.length); + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); }; return ( @@ -43,10 +53,10 @@ export const FilterCreatedBy: React.FC = observer((props: Props) => { /> {previewEnabled && (
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( + {sortedOptions ? ( + sortedOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((memberId) => { + {sortedOptions.slice(0, itemsToRender).map((memberId) => { const member = getUserDetails(memberId); if (!member) return null; @@ -60,13 +70,13 @@ export const FilterCreatedBy: React.FC = observer((props: Props) => { /> ); })} - {filteredOptions.length > 5 && ( + {sortedOptions.length > 5 && ( )} diff --git a/web/components/issues/issue-layouts/filters/header/filters/cycle.tsx b/web/components/issues/issue-layouts/filters/header/filters/cycle.tsx index 396addde6..b3a65a399 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/cycle.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/cycle.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import sortBy from "lodash/sortBy"; import { observer } from "mobx-react"; // components @@ -31,16 +31,24 @@ export const FilterCycle: React.FC = observer((props) => { const cycleIds = projectId ? getProjectCycleIds(projectId) : undefined; const cycles = cycleIds?.map((projectId) => getCycleById(projectId)!) ?? null; const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = sortBy( - cycles?.filter((cycle) => cycle.name.toLowerCase().includes(searchQuery.toLowerCase())), - (cycle) => cycle.name.toLowerCase() - ); + + const sortedOptions = useMemo(() => { + const filteredOptions = (cycles || []).filter((cycle) => + cycle.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return sortBy(filteredOptions, [ + (cycle) => !appliedFilters?.includes(cycle.id), + (cycle) => cycle.name.toLowerCase(), + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); const handleViewToggle = () => { - if (!filteredOptions) return; + if (!sortedOptions) return; - if (itemsToRender === filteredOptions.length) setItemsToRender(5); - else setItemsToRender(filteredOptions.length); + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); }; const cycleStatus = (status: TCycleGroups) => (status ? status.toLocaleLowerCase() : "draft") as TCycleGroups; @@ -54,10 +62,10 @@ export const FilterCycle: React.FC = observer((props) => { /> {previewEnabled && (
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( + {sortedOptions ? ( + sortedOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((cycle) => ( + {sortedOptions.slice(0, itemsToRender).map((cycle) => ( = observer((props) => { activePulse={cycleStatus(cycle?.status) === "current" ? true : false} /> ))} - {filteredOptions.length > 5 && ( + {sortedOptions.length > 5 && ( )} diff --git a/web/components/issues/issue-layouts/filters/header/filters/labels.tsx b/web/components/issues/issue-layouts/filters/header/filters/labels.tsx index 42e955535..7097b1337 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/labels.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/labels.tsx @@ -1,5 +1,6 @@ -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import { observer } from "mobx-react"; +import sortBy from "lodash/sortBy"; // components import { Loader } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; @@ -26,13 +27,23 @@ export const FilterLabels: React.FC = observer((props) => { const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = labels?.filter((label) => label.name.toLowerCase().includes(searchQuery.toLowerCase())); + const sortedOptions = useMemo(() => { + const filteredOptions = (labels || []).filter((label) => + label.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return sortBy(filteredOptions, [ + (label) => !(appliedFilters ?? []).includes(label.id), + (label) => label.name.toLowerCase(), + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); const handleViewToggle = () => { - if (!filteredOptions) return; + if (!sortedOptions) return; - if (itemsToRender === filteredOptions.length) setItemsToRender(5); - else setItemsToRender(filteredOptions.length); + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); }; return ( @@ -44,10 +55,10 @@ export const FilterLabels: React.FC = observer((props) => { /> {previewEnabled && (
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( + {sortedOptions ? ( + sortedOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((label) => ( + {sortedOptions.slice(0, itemsToRender).map((label) => ( = observer((props) => { title={label.name} /> ))} - {filteredOptions.length > 5 && ( + {sortedOptions.length > 5 && ( )} diff --git a/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx b/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx index 4d2839b2c..80c16478a 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx @@ -1,5 +1,6 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { observer } from "mobx-react-lite"; +import sortBy from "lodash/sortBy"; // hooks import { Loader, Avatar } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; @@ -24,15 +25,23 @@ export const FilterMentions: React.FC = observer((props: Props) => { const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = memberIds?.filter( - (memberId) => getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) - ); + const sortedOptions = useMemo(() => { + const filteredOptions = (memberIds || []).filter((memberId) => + getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return sortBy(filteredOptions, [ + (memberId) => !(appliedFilters ?? []).includes(memberId), + (memberId) => getUserDetails(memberId)?.display_name.toLowerCase(), + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); const handleViewToggle = () => { - if (!filteredOptions) return; + if (!sortedOptions) return; - if (itemsToRender === filteredOptions.length) setItemsToRender(5); - else setItemsToRender(filteredOptions.length); + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); }; return ( @@ -44,10 +53,10 @@ export const FilterMentions: React.FC = observer((props: Props) => { /> {previewEnabled && (
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( + {sortedOptions ? ( + sortedOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((memberId) => { + {sortedOptions.slice(0, itemsToRender).map((memberId) => { const member = getUserDetails(memberId); if (!member) return null; @@ -61,13 +70,13 @@ export const FilterMentions: React.FC = observer((props: Props) => { /> ); })} - {filteredOptions.length > 5 && ( + {sortedOptions.length > 5 && ( )} diff --git a/web/components/issues/issue-layouts/filters/header/filters/module.tsx b/web/components/issues/issue-layouts/filters/header/filters/module.tsx index 812cf939f..6b6cd2b4d 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/module.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/module.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import sortBy from "lodash/sortBy"; import { observer } from "mobx-react"; // components @@ -29,16 +29,24 @@ export const FilterModule: React.FC = observer((props) => { const moduleIds = projectId ? getProjectModuleIds(projectId) : undefined; const modules = moduleIds?.map((projectId) => getModuleById(projectId)!) ?? null; const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = sortBy( - modules?.filter((module) => module.name.toLowerCase().includes(searchQuery.toLowerCase())), - (module) => module.name.toLowerCase() - ); + + const sortedOptions = useMemo(() => { + const filteredOptions = (modules || []).filter((module) => + module.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return sortBy(filteredOptions, [ + (module) => !appliedFilters?.includes(module.id), + (module) => module.name.toLowerCase(), + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); const handleViewToggle = () => { - if (!filteredOptions) return; + if (!sortedOptions) return; - if (itemsToRender === filteredOptions.length) setItemsToRender(5); - else setItemsToRender(filteredOptions.length); + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); }; return ( @@ -50,10 +58,10 @@ export const FilterModule: React.FC = observer((props) => { /> {previewEnabled && (
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( + {sortedOptions ? ( + sortedOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((cycle) => ( + {sortedOptions.slice(0, itemsToRender).map((cycle) => ( = observer((props) => { title={cycle.name} /> ))} - {filteredOptions.length > 5 && ( + {sortedOptions.length > 5 && ( )} diff --git a/web/components/issues/issue-layouts/filters/header/filters/project.tsx b/web/components/issues/issue-layouts/filters/header/filters/project.tsx index b9f864b4b..b97001b00 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/project.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/project.tsx @@ -1,5 +1,6 @@ -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import { observer } from "mobx-react"; +import sortBy from "lodash/sortBy"; // components import { Loader } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; @@ -26,13 +27,23 @@ export const FilterProjects: React.FC = observer((props) => { // derived values const projects = workspaceProjectIds?.map((projectId) => getProjectById(projectId)!) ?? null; const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = projects?.filter((project) => project.name.toLowerCase().includes(searchQuery.toLowerCase())); + + const sortedOptions = useMemo(() => { + const filteredOptions = (projects || []).filter((project) => + project.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + return sortBy(filteredOptions, [ + (project) => !(appliedFilters ?? []).includes(project.id), + (project) => project.name.toLowerCase(), + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); const handleViewToggle = () => { - if (!filteredOptions) return; + if (!sortedOptions) return; - if (itemsToRender === filteredOptions.length) setItemsToRender(5); - else setItemsToRender(filteredOptions.length); + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); }; return ( @@ -44,10 +55,10 @@ export const FilterProjects: React.FC = observer((props) => { /> {previewEnabled && (
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( + {sortedOptions ? ( + sortedOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((project) => ( + {sortedOptions.slice(0, itemsToRender).map((project) => ( = observer((props) => { title={project.name} /> ))} - {filteredOptions.length > 5 && ( + {sortedOptions.length > 5 && ( )} diff --git a/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx b/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx index 06c1aae9f..e283112be 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; - +import sortBy from "lodash/sortBy"; // components import { StateGroupIcon } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; diff --git a/web/components/issues/issue-layouts/filters/header/filters/state.tsx b/web/components/issues/issue-layouts/filters/header/filters/state.tsx index 5dde1d279..2c2cca53b 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/state.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/state.tsx @@ -1,5 +1,6 @@ -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import { observer } from "mobx-react"; +import sortBy from "lodash/sortBy"; // components import { Loader, StateGroupIcon } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; @@ -22,13 +23,18 @@ export const FilterState: React.FC = observer((props) => { const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = states?.filter((s) => s.name.toLowerCase().includes(searchQuery.toLowerCase())); + const sortedOptions = useMemo(() => { + const filteredOptions = (states ?? []).filter((s) => s.name.toLowerCase().includes(searchQuery.toLowerCase())); + + return sortBy(filteredOptions, [(s) => !(appliedFilters ?? []).includes(s.id), (s) => s.name.toLowerCase()]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); const handleViewToggle = () => { - if (!filteredOptions) return; + if (!sortedOptions) return; - if (itemsToRender === filteredOptions.length) setItemsToRender(5); - else setItemsToRender(filteredOptions.length); + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); }; return ( @@ -40,10 +46,10 @@ export const FilterState: React.FC = observer((props) => { /> {previewEnabled && (
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( + {sortedOptions ? ( + sortedOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((state) => ( + {sortedOptions.slice(0, itemsToRender).map((state) => ( = observer((props) => { title={state.name} /> ))} - {filteredOptions.length > 5 && ( + {sortedOptions.length > 5 && ( )} diff --git a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx index 5f308fbd1..ce0a9943e 100644 --- a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx @@ -68,7 +68,7 @@ export const CycleLayoutRoot: React.FC = observer(() => { if (!workspaceSlug || !projectId || !cycleId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); issuesFilter.updateFilters( workspaceSlug.toString(), diff --git a/web/components/issues/issue-layouts/roots/module-layout-root.tsx b/web/components/issues/issue-layouts/roots/module-layout-root.tsx index 0c6ba3b66..268a2c60c 100644 --- a/web/components/issues/issue-layouts/roots/module-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/module-layout-root.tsx @@ -59,7 +59,7 @@ export const ModuleLayoutRoot: React.FC = observer(() => { if (!workspaceSlug || !projectId || !moduleId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); issuesFilter.updateFilters( workspaceSlug.toString(), From 94327b83112d9e5c22366aa2538839d6ac6e04bd Mon Sep 17 00:00:00 2001 From: Manish Gupta <59428681+mguptahub@users.noreply.github.com> Date: Fri, 8 Mar 2024 15:16:32 +0530 Subject: [PATCH 068/308] chore: feature build process optimization (#3907) * process changed to build tar and use for deployment * fixes --- .github/workflows/feature-deployment.yml | 186 +++++++++++++++++++---- 1 file changed, 155 insertions(+), 31 deletions(-) diff --git a/.github/workflows/feature-deployment.yml b/.github/workflows/feature-deployment.yml index 7b9f5ffcc..12549cff5 100644 --- a/.github/workflows/feature-deployment.yml +++ b/.github/workflows/feature-deployment.yml @@ -4,70 +4,194 @@ on: workflow_dispatch: inputs: web-build: - required: true + required: false + description: 'Build Web' type: boolean default: true space-build: - required: true + required: false + description: 'Build Space' type: boolean default: false +env: + BUILD_WEB: ${{ github.event.inputs.web-build }} + BUILD_SPACE: ${{ github.event.inputs.space-build }} + jobs: + setup-feature-build: + name: Feature Build Setup + runs-on: ubuntu-latest + steps: + - name: Checkout + run: | + echo "BUILD_WEB=$BUILD_WEB" + echo "BUILD_SPACE=$BUILD_SPACE" + outputs: + web-build: ${{ env.BUILD_WEB}} + space-build: ${{env.BUILD_SPACE}} + + feature-build-web: + if: ${{ needs.setup-feature-build.outputs.web-build == 'true' }} + needs: setup-feature-build + name: Feature Build Web + runs-on: ubuntu-latest + env: + AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }} + AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }} + NEXT_PUBLIC_API_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }} + steps: + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + - name: Install AWS cli + run: | + sudo apt-get update + sudo apt-get install -y python3-pip + pip3 install awscli + - name: Checkout + uses: actions/checkout@v4 + with: + path: feature-preview + - name: Install Dependencies + run: | + cd $GITHUB_WORKSPACE/feature-preview + yarn install + - name: Build Web + id: build-web + run: | + cd $GITHUB_WORKSPACE/feature-preview + yarn build --filter=web + cd $GITHUB_WORKSPACE + + TAR_NAME="web.tar.gz" + tar -czf $TAR_NAME ./feature-preview + + FILE_EXPIRY=$(date -u -d "+2 days" +"%Y-%m-%dT%H:%M:%SZ") + aws s3 cp $TAR_NAME s3://${{ env.AWS_BUCKET }}/${{github.sha}}/$TAR_NAME --expires $FILE_EXPIRY + + feature-build-space: + if: ${{ needs.setup-feature-build.outputs.space-build == 'true' }} + needs: setup-feature-build + name: Feature Build Space + runs-on: ubuntu-latest + env: + AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }} + AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }} + NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1 + outputs: + do-build: ${{ needs.setup-feature-build.outputs.space-build }} + s3-url: ${{ steps.build-space.outputs.S3_PRESIGNED_URL }} + steps: + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + - name: Install AWS cli + run: | + sudo apt-get update + sudo apt-get install -y python3-pip + pip3 install awscli + - name: Checkout + uses: actions/checkout@v4 + with: + path: plane + - name: Install Dependencies + run: | + cd $GITHUB_WORKSPACE/plane + yarn install + - name: Build Space + id: build-space + run: | + cd $GITHUB_WORKSPACE/plane + yarn build --filter=space + cd $GITHUB_WORKSPACE + + TAR_NAME="space.tar.gz" + tar -czf $TAR_NAME ./plane + + FILE_EXPIRY=$(date -u -d "+2 days" +"%Y-%m-%dT%H:%M:%SZ") + aws s3 cp $TAR_NAME s3://${{ env.AWS_BUCKET }}/${{github.sha}}/$TAR_NAME --expires $FILE_EXPIRY + feature-deploy: + if: ${{ always() && (needs.setup-feature-build.outputs.web-build == 'true' || needs.setup-feature-build.outputs.space-build == 'true') }} + needs: [feature-build-web, feature-build-space] name: Feature Deploy runs-on: ubuntu-latest env: - KUBE_CONFIG_FILE: ${{ secrets.KUBE_CONFIG }} - BUILD_WEB: ${{ (github.event.inputs.web-build == '' && true) || github.event.inputs.web-build }} - BUILD_SPACE: ${{ (github.event.inputs.space-build == '' && false) || github.event.inputs.space-build }} - + AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }} + AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }} + KUBE_CONFIG_FILE: ${{ secrets.FEATURE_PREVIEW_KUBE_CONFIG }} steps: + - name: Install AWS cli + run: | + sudo apt-get update + sudo apt-get install -y python3-pip + pip3 install awscli - name: Tailscale uses: tailscale/github-action@v2 with: oauth-client-id: ${{ secrets.TAILSCALE_OAUTH_CLIENT_ID }} oauth-secret: ${{ secrets.TAILSCALE_OAUTH_SECRET }} tags: tag:ci - - name: Kubectl Setup run: | - curl -LO "https://dl.k8s.io/release/${{secrets.KUBE_VERSION}}/bin/linux/amd64/kubectl" + curl -LO "https://dl.k8s.io/release/${{ vars.FEATURE_PREVIEW_KUBE_VERSION }}/bin/linux/amd64/kubectl" chmod +x kubectl mkdir -p ~/.kube echo "$KUBE_CONFIG_FILE" > ~/.kube/config chmod 600 ~/.kube/config - - name: HELM Setup run: | curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 chmod 700 get_helm.sh ./get_helm.sh - - name: App Deploy run: | - helm --kube-insecure-skip-tls-verify repo add feature-preview ${{ secrets.FEATURE_PREVIEW_HELM_CHART_URL }} - GIT_BRANCH=${{ github.ref_name }} - APP_NAMESPACE=${{ secrets.FEATURE_PREVIEW_NAMESPACE }} + WEB_S3_URL="" + if [ ${{ env.BUILD_WEB }} == true ]; then + WEB_S3_URL=$(aws s3 presign s3://${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}/${{github.sha}}/web.tar.gz --expires-in 3600) + fi - METADATA=$(helm install feature-preview/${{ secrets.FEATURE_PREVIEW_HELM_CHART_NAME }} \ - --kube-insecure-skip-tls-verify \ - --generate-name \ - --namespace $APP_NAMESPACE \ - --set shared_config.git_repo=${{github.server_url}}/${{ github.repository }}.git \ - --set shared_config.git_branch="$GIT_BRANCH" \ - --set web.enabled=${{ env.BUILD_WEB }} \ - --set space.enabled=${{ env.BUILD_SPACE }} \ - --output json \ - --timeout 1000s) + SPACE_S3_URL="" + if [ ${{ env.BUILD_SPACE }} == true ]; then + SPACE_S3_URL=$(aws s3 presign s3://${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}/${{github.sha}}/space.tar.gz --expires-in 3600) + fi - APP_NAME=$(echo $METADATA | jq -r '.name') + if [ ${{ env.BUILD_WEB }} == true ] || [ ${{ env.BUILD_SPACE }} == true ]; then - INGRESS_HOSTNAME=$(kubectl get ingress -n feature-builds --insecure-skip-tls-verify \ - -o jsonpath='{.items[?(@.metadata.annotations.meta\.helm\.sh\/release-name=="'$APP_NAME'")]}' | \ - jq -r '.spec.rules[0].host') + helm --kube-insecure-skip-tls-verify repo add feature-preview ${{ vars.FEATURE_PREVIEW_HELM_CHART_URL }} - echo "****************************************" - echo "APP NAME ::: $APP_NAME" - echo "INGRESS HOSTNAME ::: $INGRESS_HOSTNAME" - echo "****************************************" + APP_NAMESPACE="${{ vars.FEATURE_PREVIEW_NAMESPACE }}" + DEPLOY_SCRIPT_URL="${{ vars.FEATURE_PREVIEW_DEPLOY_SCRIPT_URL }}" + + METADATA=$(helm --kube-insecure-skip-tls-verify install feature-preview/${{ vars.FEATURE_PREVIEW_HELM_CHART_NAME }} \ + --generate-name \ + --namespace $APP_NAMESPACE \ + --set ingress.primaryDomain=${{vars.FEATURE_PREVIEW_PRIMARY_DOMAIN || 'feature.plane.tools' }} \ + --set web.image=${{vars.FEATURE_PREVIEW_DOCKER_BASE}} \ + --set web.enabled=${{ env.BUILD_WEB || false }} \ + --set web.artifact_url=$WEB_S3_URL \ + --set space.image=${{vars.FEATURE_PREVIEW_DOCKER_BASE}} \ + --set space.enabled=${{ env.BUILD_SPACE || false }} \ + --set space.artifact_url=$SPACE_S3_URL \ + --set shared_config.deploy_script_url=$DEPLOY_SCRIPT_URL \ + --output json \ + --timeout 1000s) + + APP_NAME=$(echo $METADATA | jq -r '.name') + + INGRESS_HOSTNAME=$(kubectl get ingress -n feature-builds --insecure-skip-tls-verify \ + -o jsonpath='{.items[?(@.metadata.annotations.meta\.helm\.sh\/release-name=="'$APP_NAME'")]}' | \ + jq -r '.spec.rules[0].host') + + echo "****************************************" + echo "APP NAME ::: $APP_NAME" + echo "INGRESS HOSTNAME ::: $INGRESS_HOSTNAME" + echo "****************************************" + fi From 8cc372679c526c096b144af1b9a2b990b1e0db2b Mon Sep 17 00:00:00 2001 From: Manish Gupta Date: Fri, 8 Mar 2024 15:56:05 +0530 Subject: [PATCH 069/308] Web Build fixes --- .github/workflows/feature-deployment.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/feature-deployment.yml b/.github/workflows/feature-deployment.yml index 12549cff5..766f30514 100644 --- a/.github/workflows/feature-deployment.yml +++ b/.github/workflows/feature-deployment.yml @@ -40,7 +40,7 @@ jobs: AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }} AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }} - NEXT_PUBLIC_API_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }} + NEXT_PUBLIC_API_BASE_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }} steps: - name: Set up Node.js uses: actions/setup-node@v4 @@ -54,21 +54,21 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - path: feature-preview + path: plane - name: Install Dependencies run: | - cd $GITHUB_WORKSPACE/feature-preview + cd $GITHUB_WORKSPACE/plane yarn install - name: Build Web id: build-web run: | - cd $GITHUB_WORKSPACE/feature-preview + cd $GITHUB_WORKSPACE/plane yarn build --filter=web cd $GITHUB_WORKSPACE TAR_NAME="web.tar.gz" - tar -czf $TAR_NAME ./feature-preview - + tar -czf $TAR_NAME ./plane + FILE_EXPIRY=$(date -u -d "+2 days" +"%Y-%m-%dT%H:%M:%SZ") aws s3 cp $TAR_NAME s3://${{ env.AWS_BUCKET }}/${{github.sha}}/$TAR_NAME --expires $FILE_EXPIRY @@ -82,6 +82,7 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }} AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }} NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1 + NEXT_PUBLIC_API_BASE_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }} outputs: do-build: ${{ needs.setup-feature-build.outputs.space-build }} s3-url: ${{ steps.build-space.outputs.S3_PRESIGNED_URL }} From 6c6b7156bbb46fa5d2ebbd4e0d851b264d5a2cd3 Mon Sep 17 00:00:00 2001 From: Manish Gupta Date: Fri, 8 Mar 2024 16:00:18 +0530 Subject: [PATCH 070/308] helm variable update --- .github/workflows/feature-deployment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/feature-deployment.yml b/.github/workflows/feature-deployment.yml index 766f30514..c5eec3cd3 100644 --- a/.github/workflows/feature-deployment.yml +++ b/.github/workflows/feature-deployment.yml @@ -182,6 +182,7 @@ jobs: --set space.enabled=${{ env.BUILD_SPACE || false }} \ --set space.artifact_url=$SPACE_S3_URL \ --set shared_config.deploy_script_url=$DEPLOY_SCRIPT_URL \ + --set shared_config.api_base_url=${{vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL}} \ --output json \ --timeout 1000s) From cb78ccad1f9f0d9266dda5c7fc365a59d562a5d1 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Fri, 8 Mar 2024 16:58:37 +0530 Subject: [PATCH 071/308] fix: 1click deployment fixes --- deploy/1-click/plane-app | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/deploy/1-click/plane-app b/deploy/1-click/plane-app index e6bd24b9e..ace0a0b79 100644 --- a/deploy/1-click/plane-app +++ b/deploy/1-click/plane-app @@ -494,13 +494,6 @@ function install() { update_env "CORS_ALLOWED_ORIGINS" "http://$MY_IP" update_config "INSTALLATION_DATE" "$(date '+%Y-%m-%d')" - - if command -v crontab &> /dev/null; then - sudo touch /etc/cron.daily/makeplane - sudo chmod +x /etc/cron.daily/makeplane - sudo echo "0 2 * * * root /usr/local/bin/plane-app --upgrade" > /etc/cron.daily/makeplane - sudo crontab /etc/cron.daily/makeplane - fi show_message "Plane Installed Successfully ✅" show_message "" @@ -606,11 +599,6 @@ function uninstall() { sudo rm $PLANE_INSTALL_DIR/variables-upgrade.env &> /dev/null sudo rm $PLANE_INSTALL_DIR/config.env &> /dev/null sudo rm $PLANE_INSTALL_DIR/docker-compose.yaml &> /dev/null - - if command -v crontab &> /dev/null; then - sudo crontab -r &> /dev/null - sudo rm /etc/cron.daily/makeplane &> /dev/null - fi # rm -rf $PLANE_INSTALL_DIR &> /dev/null show_message "- Configuration Cleaned ✅" From cead56cc526af19638141f9ed25d200c8209cf4e Mon Sep 17 00:00:00 2001 From: Ramesh Kumar Chandra <31303617+rameshkumarchandra@users.noreply.github.com> Date: Fri, 8 Mar 2024 17:32:00 +0530 Subject: [PATCH 072/308] [WEB - 671] chore: remove unused images in the app (#3901) --- .../empty-state/Project_full_screen.svg | 31 ---------- web/public/empty-state/analytics.svg | 21 ------- web/public/empty-state/dashboard.svg | 23 -------- web/public/empty-state/estimate.svg | 19 ------ web/public/empty-state/integration.svg | 15 ----- web/public/empty-state/issue-archive.svg | 3 - web/public/empty-state/my-issues.svg | 15 ----- web/public/empty-state/page.svg | 21 ------- web/public/onboarding/sign-in.webp | Bin 21740 -> 0 bytes web/public/theme-mode/custom-mode.svg | 54 ------------------ web/public/theme-mode/custom-theme-banner.svg | 33 ----------- web/public/theme-mode/dark-high-contrast.svg | 34 ----------- web/public/theme-mode/dark-mode.svg | 41 ------------- web/public/theme-mode/light-high-contrast.svg | 42 -------------- web/public/theme-mode/light-mode.svg | 42 -------------- web/public/web-view-spinner.png | Bin 1456 -> 0 bytes 16 files changed, 394 deletions(-) delete mode 100644 web/public/empty-state/Project_full_screen.svg delete mode 100644 web/public/empty-state/analytics.svg delete mode 100644 web/public/empty-state/dashboard.svg delete mode 100644 web/public/empty-state/estimate.svg delete mode 100644 web/public/empty-state/integration.svg delete mode 100644 web/public/empty-state/issue-archive.svg delete mode 100644 web/public/empty-state/my-issues.svg delete mode 100644 web/public/empty-state/page.svg delete mode 100644 web/public/onboarding/sign-in.webp delete mode 100644 web/public/theme-mode/custom-mode.svg delete mode 100644 web/public/theme-mode/custom-theme-banner.svg delete mode 100644 web/public/theme-mode/dark-high-contrast.svg delete mode 100644 web/public/theme-mode/dark-mode.svg delete mode 100644 web/public/theme-mode/light-high-contrast.svg delete mode 100644 web/public/theme-mode/light-mode.svg delete mode 100644 web/public/web-view-spinner.png diff --git a/web/public/empty-state/Project_full_screen.svg b/web/public/empty-state/Project_full_screen.svg deleted file mode 100644 index ce3738507..000000000 --- a/web/public/empty-state/Project_full_screen.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/analytics.svg b/web/public/empty-state/analytics.svg deleted file mode 100644 index 4f7cd4b7d..000000000 --- a/web/public/empty-state/analytics.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/dashboard.svg b/web/public/empty-state/dashboard.svg deleted file mode 100644 index 1e45c69ee..000000000 --- a/web/public/empty-state/dashboard.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/estimate.svg b/web/public/empty-state/estimate.svg deleted file mode 100644 index 604b2f143..000000000 --- a/web/public/empty-state/estimate.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/integration.svg b/web/public/empty-state/integration.svg deleted file mode 100644 index d4f8ec5e8..000000000 --- a/web/public/empty-state/integration.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/issue-archive.svg b/web/public/empty-state/issue-archive.svg deleted file mode 100644 index eee79ff3a..000000000 --- a/web/public/empty-state/issue-archive.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/web/public/empty-state/my-issues.svg b/web/public/empty-state/my-issues.svg deleted file mode 100644 index 98a420d75..000000000 --- a/web/public/empty-state/my-issues.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/page.svg b/web/public/empty-state/page.svg deleted file mode 100644 index 2b0e88fcf..000000000 --- a/web/public/empty-state/page.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/onboarding/sign-in.webp b/web/public/onboarding/sign-in.webp deleted file mode 100644 index 0c7fb571c55d1f0ccbfed2af6554c8d0cbd9e958..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21740 zcmbT6Q(Gkr7lwDXU6XB1w(Xj1H`%sr+ivID*_dqGu1OQW_cwg&y4J}$eopT7+^RB? zl1J(QfTo0~@-Jm>EqDL`fcoEBf&+}f03!0r(&mT&02p4Y9R)4#=odk6#o1xxmy)o6 zneX2|q?b5=1t1Xvz{Z|bLS=*>{>p{yx`oCH#y03-R;2ZdLZ-M*zQ7Uyrv#4f2vBvr zE8UNc=jMBdJklJ7?8gq|v_Bkf3I&{6OLgBs(2TWRZzw*4oEhaIQ$=6xbS48)fBqZsBZ}ZahPryxzPW zw+jg(Nv$qc?0&nWpG7bSAjz#RYwr4b+kc-{?4NrBcTARWRGKf|D1mE-i-to>Pwenc z#P{Zf_z7>hitpt9svQ19N$>YY92fm)csmhB9QI(`W|c(bg1)%*gT@Eks}L(4ZhzXm zgT@ZQ=pRGODm~`ld!{?n`qG#>x?uJ+3YYI^0yTbU2-xnU)kCA2Cot$0J1^J0QN!-7q zw)~>XtJ6MnnujA-sR#t4S#d%%DVL+84-^6B1F(r{0U0oihsY9t^qWWK=g9gGFNTYc z!RJ~N`kxjQhei+PbX=hfnu_ZtH*p@5x=Y#PcTfm=yHtPq2ExO0QgUgxq+5VunS;R6LmGF*X@7v5TZWMd!oq<=% zS?TDYzYaAGl?9qrhfTs`9zSoG$KQQ-jb6d)0@%SAew~Jwc09MZ`QO5Z)zl)lt=j92 zt7n{Bv9=CDR4 z)*LQ3hTwu|yorTetw~~*c;gG*Z)qq2KxAH2O$}fYqiBD3v1m-}~ifHY`?7 zP+C$rPBS+^0LxKK6Q$U=UR&g2$C{r0;5Ek;TI%ziY`UDW&2hV@+W>;ZesE=$(wzjY zsf}|260jNL&%D$s`JRbSAnQQ!Cin-3w=N_j&hk}TZGZ9>UsbU4B z+7X=5jI9@Do<hTj6hDSCicPK2wdzuj<5c>!a_^lIWU#=|%S{Z0TtsB9btG7mF(&QzQ1G4KP=)dWum8WN*e;4xle&Y^bSzAB7 zlKth?YK80tl^((U7hOGsI)bL0TH9j*j^|ZdjoD%pS2$7y?mDN|>lS2#>uMH)HU?91 zta`oYeyEZw2~c#V^`#eSRAJ=y53ELcBMw(H@l>W9JUN0=tisQp)=*}X>BgZjx8C%8 z`#Du`vTf|PRI}fKaAvlsDO0R^1sBb&Z`1X3`jUx|P9V@tq6!YxTcx>5(@*r%ePe`Z zOSgOU)P^7EQp@L)q`dk-Mk@%17;M>;`07L~euryFoT%E!8YTII>W1~bMkg``fzJkX z(KXu887yE4(}Z$wWa64BdRffgWN|CWq*yutG9))cH3A2y+i~Rq{)eW#j02%2Tpg{U zNSKd$u~j*KFdc4?pSi(4sD$1$)i~zb0frIe>Sk2&hJaa1;#3!-6QRbS(l4@3WiB!l6p zJ^X7Dt+}WUUcqjXzScK`vI?%j>EJcPO961AcN!uOv>dZ>T{yy3&pc0SMd67pc;H2`fSfG(qT0vO*PXnb{0ta4#b*M zQd0x@47H479?&R?&{pu+^ve}~m$zW!+&TBF!&*9Xw^vSZ7d)_;etO>|x*+e2ra+N6v23SDCo zku!IyVKAd*)jA!-Hmg?YDu!Y|vxd4bUTzu*vu>YUg;X#EnWaYFJySUQW_)nh0(4prCfm+5QSWN+{L3A#4#I^Il1m-Pi7cjx{K{4Lvp zPtTu+QoH`0S#6gQ$vFaKXmg97?YI6tpbZ%w|77`)E6L&9!S|P&ozBnI7)x`Z%%92e zFtO!#4PoVT@}hz0WXn*Tw~H4NJ83-7PHdl~4+yG$p(uDJtZ{aG{Z; zL{mCMs+8Qs5LU4{UnD;9HLd>N^C$aI`1wfT<2s zD?%U}m1|r9tUM0tR-iIXq$)cpWuzzz1vi+EtrpcOLm3tTYEr38Tf~K8s8%mSIT4oX z4sk@qyQcqG{Uy*0)dK!If#+y$E+vm2ADvxKpTHHU)5Bo}q_X2#o@|B)?1V zk|=>oO>2uS?KbMKF)9}+V}eG7uVVQ5V3uWb%U1o)jW1;wP$c|wturo1<-t#5@93!u z4KX_cP(`WCp65keZQlbAN39G@rSW-|pTs}~?*)ftJEqwr3#D7n2OOxivl$NBWP;Tt zcB9HQ2X&`T|MB^^q)%gg`ZTCVUSvOR%v72L`H)!q)}i^!&h%3P6bh)*>!d;hpQ;?r zCPKtC;FshS?H`DoCJ+3negc`HI31ib9ongwp|(vOHw^X6yXHH5^>l|6^YadGHk*%I z*@oPY`WL*<#-wzKb?!%27f@8bbg*$VZ{Rx`j6D(iOh8r7nH^K!>UK!XbAUHs<>k9voYX0`ZT}w zZIsnTyJ@FgFc9HpvF%ByzZ{wpigQx);ZFJQ!Dc_{pkBB!YVdI&xEr$kuz~G7@?c#fW8w`5w|4? zzao<>*<~DT4e-NR`d(>IQq*KZ!c6Q9l$3<{(8}+nGX{#iK$g9aNBdLZG@{vme2BffGFS|< zdp$mR1F}i8+DAngi?0p-z?OIkl#XR)e=sjm%K&S?z#fG+%a4ZJzH@;hL4wbS*IAWs z!LQ}7+mF8Egf*B4q4R<(p{_3_-<+@ZuZSq6g@EDjg>Sw)s1L(8%{QStp*^AJfbj4C z=6?2__=EUFXxsnfUHKE?WA(G)p)WnaC}1+6F<|i z`fyoSf%K+X8_2A0Ni&LaUP(0 zCCP4tW^^6*WMs}Wcry`V!v5<2PU0yNKutT;iWxuoJ7swnSzm({e( zV?*tJWUJ`#Yhd=09(4Eb|BS-~_(rMy#0$hDLTSZ;c75c1&2Ahqs=A!`iE@HYZMs!1 z9f9r?wq)0&t4}d+SwbDOa54vhVMsd!>E-PBfIG!)Q8sY)Ajsuq8 zW`DtO*S8Yt&F^8Cg0Wv6h)r&3;+l!K79pxwo_^E){n30iB^SJQvDrQzZ>L&VB48>! zEou0dr3XF+tnepOj>a zN<11}jatRF`93S%pVbvI3Fg}mpN0EvSM}^{7836!5EPt+QyqOx>6%+*rL|9&lJotP z;FJP{MIq0aRnyAxcM6K2!>94sY?gry#;Cy$S6Lk3cRVnPE$ggtjI-holE{8kr5{ed z_DoiGpV$nhmPsjr&7YTQx(%fEz#9AU!;9KviK_f&90yVVg5`BUo-A$80G~_IhhmOz zSr4~yunN1w3QBYiECu-MuPFQ(m44}M?OT8Gm~e|IYFkI?_c#IY1V8NkiV|VmtwpXR zS!lZu;~af-E_T+pTzwdoT{(yaf0UuYydr3+HWhZs_F+Mcr1&~J@90E*1afI>JhEnK zJwiCDz)7=m;oRtvU1Yv{(#<1UIV=1)p(A>_8Pt7$lCl>85|R04k#r}A<~MoQ4aXZd z-eMqHK4IM!_2OL6cS!grA&}*i&1i4o>pE_IN&Z3gEqc0e`A}An(DYl*C4=$LA4xRn znQpmtRq9%Co0)kKUHS=2vY@0%h13A6;l4bPd0N8hyK0&oFPt z-Uo4vNRoec=0IBjUanflir+?HG0@I-K$otuV03F|L!n3LGU&-DJ4wzPs+K_%^NsX~YjbZ4Ku%0|(u}nzmxg^Yipqsj` zlA+ylnIDOrvwz?d$xooB3XDzNV`OuByG*-^8(2xT0qjodRHhBdmdgc}D7_F5!UstCq;lX=a2zfxPB zAakK*stC7AxINn$o3tF*nx19ITIhfi0fgFPUnb5$w6Ceoc<@X3Sb&V+c?v;6Kjqs$ zM2o3_`#7^SD9>-br)f5}wt2AcZJ*p9v5kGO9S-#U4_~-@YquXS%Ys2E|MLpReWhh~$wLk<01QNOh!813`|J&J!>4nYZ{i813rw#%{JDYaT-+Ss~V! zw~(lHN4r(PjmaG^*xI_cX>7$Os6_h`J#BdYS@TKI-96&4+fRHe={GvbxF$d zJT>+3FV0|tii&a}V;J67t_tv#ON!%o&xZ`5V#&+C5~9kl?m97j_OG(6*nXtEDl|B% zzB-?fJ*&eTTD{b;3{aQAPs{1l!1LOH@F^LoVm@WNOyJm*MF9q!j&m3F$J@Z0{R4HP z&;a=ZFWyBbnaxYdY#Z9%pP7=9h=*EPpX|a-{M&0Dnj-cbTyRdT<*IVyEHf;%V?=1a zK>_y0qZmkcqgH_oqk1jUByH1YybOZ`!t^zKToIxn3GE%Nuqb!^5*_J+MQH|3Vkz7t zNzUV{XBd`pq~2VP)|vE!p%)&9{72LfK32H%???n4T1Ovf(EDQ>2a6tNg^dDTh?B;= zaZ=XBp_GPuCih12EWC{UMQYYzsJeb_*inv*%GwyDOirII#-068M|gpNtCB}cPLwGa zH}7ueguH-8`wbDMeDUvV@57rumbLz>G{c z(<}n+P=d&kC0TCi-lF8z2P985tJbtgk)~9-ujtH}K7dBt`L)^+vBE}NrzDvxTPMmV zEoFfOwL~XRTh0HrC+XNXF=d1hl@f`~Z@RMhX9XCd;KZ;A? zhdr8#LD>yZrG+0p|HkdKdPl*y&a&F7;VJrUEu!esba$%eh%aq=X*6H^(AQj$_y zA`$B$CPZHR3-GX;gwU;h!8w;(b<(LBp($u;1t7g(B=yUaaf?=uapWh#d90+(K1YlP z)I_)_yvPi-S2-Si2evX?)bHM4fRfzm3UX>e-F{I$f9^oo^U%$*uOH>;0OaRvZ3(-M zigo)eAHvr=kcTmf$Q(lYb7f(z(sk%i5U5Ajfp0z-EF#(K-SP3XYmTXR=0f}KcP943 zQ;T0Vvf?+~YKO@yCB^%7%H?R>3B&D0TBKL@#F`pcFP#w~Bw7)xxt+K7U(7pBzbYmo z`Y-D=F?B*Q^Q55Z_>P;Br*cyK$>k+3yr$~4&w?v|XwJ;2d$gx~9rZv0d?L{v0UMQ? zmP;zFwACLIn_;@H5Pkl{vRmh}G)8p%!;@>xuP zg~rDX5NY^#)!3Hm4;1j0ton3@0B3fPQTw+p9(G~}Ce>Z|DZ*(ofM~5SOIz_tgALfM zoUKfYKTAb}*Mvm0)y+t4U!iGv{T-n3I~VVj3$$`2Etoc$G5on}3^78|m-D?}{Oo~? zs8Mn>PPJ}gUkbq$0N7_zOkJ5&u)P27C>QDB;}ik>dvKboLETb^YWz!)42pHcS{<+q01?aBZo5}p=#=0lIm5!L%}plE!-;9kt8+)1+-CDCT_ zjDQ=AE4Om+y?c+2^I?xJ*vMjQfKfPTYiB@BnK9Z30Dy~v!U?)DMf`=9@BILJS89xS$5{XcHrgC0X=S8$ly~Oo5_ZTAc;==y-Aj1tIG*T_P4!^D)V7;+-p-O z0jvlQzVEwdn#Z^^))-@_^3Tz&P-~H?+MSYt*0TRj_R_PleA<6DJ6{X4$@_yp{+!5o zV;X;YIv14+(5_`7nlW7sT$)=HU7&s6K1w_4ExhX||{mF}5L#$RU?^F`2(y7W;9u z%Z!#$9Kim;p{~qdF*btTzA-Yr^}eH&@A{5)|Lf87EAfj&Q-)oL8qG}aBu{;$RPtBH z!Ix6bv`5J!4DPqusk$?Gi*)}e3&U-Fc_75zPDz2PL7?u}ST zmX-m=9TC$s46e6ck!qpcNiELa7TMAG`ye@AqbDXyX|YwQmQCz3xurFp;z{j&3+=C? z0UGn))7FSx_!LT$Qd&#;m2ncfG4K)`-q4RYF0U0j(oTmcxxZT=a1b#8j*U9%BMtdD&_=kwS~Ie@Ek3w+H?Yid&q>u5Ebc0Hihl& zNNV|x-)qGu3w7&b5mPHUw_b9@zq9hu07jj}Lf?9IZLbf}4h{d-&)Gn68?cQ5e26KP z%}Afxg#hHi%pbq#LbUp?GaeQUlxJq07H=yQF1ZrLHZ;Su_#GhnjXo|O4z2+y4phV~*|8SjP$Kj6`1x$Y;m-%u3otRmi z%}F^NYvO4XNPOd#wJkmhBr?{T;lF|aWSYzZF6rE+|MVQy;J@k^hx9)A>1+D=Xh#MO zfA#CtovS%ts@Uomnipq4$YWj3# zRh`@JTO|G51f!xu)NDFb)sqCQcv(V#A(G!^v(8IY!aFl ztK*y)Sf+cI0`bgaohcXWKbyqyL7d&F$DpvF%MQC8R-Z&h@)STp*axV3OW{t$h^j-> zew!Gv%;E`-tDi&|G8$KP-;oG9%Q>9j`W?Xa#d!81YzGmKghqAaYQ<*2)Mcibj-j5K z;W(Qfh|>#Thd=pDD_28&gKRq#BJ1Du zUxRJ6bu(mVMzmx8YGD>2M#@AO{=d3S1g@_Ik5Vt&@u9BNeNp%SwDb<3LLVTb^yMut zdN0=uB)CZJpxpMm>k8II66GNLsk%+A?BPIQ+pjt_TewHw+yw}#+cW0(qc2zT(`&5} zoWszb&0-LWl9HiC*Tj~y!)lQ}s@>1hgKZfu%&)gbT?3o)*O!%s zHy)E?(=J-tR}Yx;^^M%oWTYXHLHcPp?>o`mPfu zvMcpMFJp&=pS60bWVwPxW!R(2$iJjCiynZN_{4J6(981bCW)<$b@tyR4yUKFE{0m- z6hYZsKfFz2bXcHpS>^V+k*)4Aev)7|e7i6lD?^`v02nnQcv%p-Tz}(j!=x9@hW5Gw zuby`4PFt13_}>how@nu)+(P`s$&AP2-w>xw@a1&T_MlpODO8;Z3dV60DtoLKPFir} zEfL+df!6ccs)d4w_LZAb;nwYDe7xL+Oxz1z8VGW6)Hz^>0VeCTxgV?D67%G_f`SNR zv;4T&*7^*g*;ZUZ@R-CSfpM=ig+fZV87Kn0+At!25vhiDw)n%Y!wdKTGHC42m26ys z(NrIyGl|uZ>7bg9BTm5gGevP!A4*@L93*V7mfSQl7QL%_Y+S!T^%5bDeNGX2#|JX3 z%-YLGaN*w~JZs=2NAC{7c76w2?JKho?;d1{CNCnmst=#tX0CLBBZ`Bq?fsjTHT;!q z2J3*)!2me5{9MHysaBd)@Oj=pe;$>A>iHduSt-?eIc@6wuT(y3>n~}e{{OH(=C`>rW2fNN z6rOEYHPG@yu@eK!B%m}0hrB&HAnKW#>mZ&N7q)t3PHM%uA13%rD;p7AHMpv&mKzJV;c z|4VYc<+h`jd^V`EoABsC z(2$(OS%>_xTKX)D)Ch(x2=R+M#1LW8&aaF9z=6!HmsS&gJY`fI{8y&eQD1ScV+$S5 zDmfwPpj_L054n^~BgG@x2If5@plu5MX@8nsl~&rr;h$1UO8jf|d1 zJx1^RWkKQ~c{WQ3v{qErc2%nk6Q&LN@TXeOR~i6?cL3Ac(3f*@{c8t}1g$qnz8 z8P5hX=iY#qBn!PB7H{!V(htQzxp!G==kX$%j;{tBx`$I zN{B`LN2mD}!fCN9rZAjjP4`|AqrSdhy{9RWr2l6(tb#vI-nqz9fSesS2p-W>G-?^e z+lZx>{@)$O=s~sE7z>;hQLR5Xv3t8W1CQQFriE*tq5fwi3y73)c?Z?lrY#JAF)i|* z1}0D}EV4)`83IVFdw99V7D;%#z5bN}A(qgy%A9WX>Vj!a9lYQQbG1CxR*Hzg7a&e9!k60FQ?^)`eJDBf$&{}|s&9~?yJngmrk5Y=W5 zP3{NXMMia~YfFqoVPYE04ES={<9DNE?S*MaXN#Fht~zjRhD=Q^U6qd8a#hAle*=4=%KGNL4sqm zgWS(k`@XC?cPBssr(mwMMA!)*MKc$h0{EplNA2w|1QbhnsUkAM{VdAL>s{!V{jD#K zlEtDnhn`4YU(8>`m;?FI&w=tFvlZ6G z?w07C{-!#+p8t7nzPge;xg5 zjsVtyGt6l;X=B87KuePI+5L)Oc+)I*T7p-Xa|D=nNOPNGk3Ua}=lp9X@dydKm-yK? zIo(bBNh@B4BErzCUo8BW!Yx614kvCGOrn-SLl1H}7r3!_J+8_55*>f7Nb>0x^mD)W z@1(SI)d}2b3(fOE7VI6kg?{&fj5f2R)?>+a9E2(o?8^pvL|~CGqo1`#G-Kuol=U|4 zi2vJstAgr0<1@n6S$yfA0JlGn9C$hhaUiMTvnHV->=SM?Uw!z>sk{`lpIRGo4dq2) z4-F*QaS-0-Mtj5MNbXy3Yo1NE^QtU-DHddfV2wO0Wg|)wH#B15-NjtM5UGaBSOf*- zH_ru?sWcJ)(Dea=HZ&aR>f0EWTdh>3^O2*2*aFN05Sm@;FDzp!Gl3HV^Ka9;Ds=3&cFDs)|j5EzmKh( zPgSLZ91WM&lHBAMqPZa6_q9K&^NqvlEs`PbKd@g#E4ex3w5NRcfz%LUWr5wzQ(XFU2{>^;L3Oyez)q%#gKc70O*bMdg()v7kx9!%y>Zibh3qr#&P_4sb0%`AyY zlK|RK^?s4k@2_O5rL5L{d+C|jH~u;*I>Bst?~T6T#5gO9A(H++n$`0tYL0xt$5bza zOg(tJlQ4TY^(2D5=T2=Qs=)k$Mw17G$Qr6%q97Kh(aB$P6vmUDFKEA;J&gRjljFbT zZ&;dz=j78&M)E$4sOg8T4tfW&B7)|itHCSqKq|FxavRo3l(4lR`xsPI9DXH>b9q#nhm2Do=c_030f(&n_VV0m<*pXoYZwQ~hS;g}Vuj2DE+VC3LFzx_5r~To4>yVC4s89=6MLz>5~A*2Ih^ ztn_^`9%MXXeYsEXMZ?n#qTMmVmz1$tn9kAVu0Kf+;+fo4V$pK)U4ke2(a9Bpv_v!> z$E4xwl^rQ^5|5707?UOWz2M5Ko_sA-`fwYFB)PS_4r(Zp z{JsXu#VcUjvcoWus-OurZK6hFX@fpawIIIbodm{9{;?wU-f0~le>2$fWt3I!E`jF^ zr#Cy?2=yLhtL8d}M5n9&F!*b9z}(w@CX@@i|wmooz8FwkKTenKQ$QE=c}S0^Jp-z;>Z|P21C0Q90y4!59L`G zt@c2@{&0~*@@Uxg*0oBrFnew~byV23uIIO=C>m@X{rF7@dnXXs>F}}PbTn{=hKOP! z8Sb{w8mdrkwPfxo|LQe<>WN^s2@u~Wzv;nb^k1dbQ||Um8A4kVn*_h z|H_y6w;AXRp$0=}5m1)l>P4cMmu$V70`;#+>fxfc(cE!pu)OfFyoYuw>XhMns|M@t zb%V;+hx0BCXYfdQ5-Mkj4`D_pwnJccco$fg$kYpqqZ#7U23VEwVe`^u(RCHj*{NRI zayDcyH+;ms@ovPZ>rZ$tJEwZ8d5Zidps*%zco;AduOah9x7*F>4d&@}h9}kXV<-I0 z$ifYKm6Dxl0)kEgKEjPE6;?9CZSilwei5P+q041#%Y+MG)UspZEd}~}?I{>L08Ck8 zdSA<=e)Iewyn$)Kar-ow+2+CBKvmy& zvuKil`pLweIs*R`8La5=>0w^6V{pR;worv7aZ_MxL@Cmo-2D~w-Eq<2qyw_==V`Wv zp(Z%BpByECGrYsXhDr0IR>7pae5?1|c<2YfSs=s@_{?CT*}Xb=(vD^O*Pj$Zvoc3K zbOP`NQ+jWN#%Ec649TC#sAfqj6u^u}1r-T@#OnnsP76de=-FhoIl$C3L5MoF>!CBe}gQ;vzmh)Ui;jf#r7EkXEkluZnh0!QXF zcb^`uy}E zG8%WzO5a(opf3Dy?+UEC?}?uP_iM6FtF?&!NMH?Be>Y5PB3(Uhy~54gqFdb zm^JChmT2?ib#QlmK>;E)F8|6rAKYXwZE@Cy{Z`X0KS@4e2j{RObHGP>jR6uRxM(lg z5U!>pH&^>pR^<#;>{(l`gZY_rz?>TqtnRq-@}InHruZM(eb3)KjGg>-mu4BkqC>k+ ztQP_3jwOz*`lj+@sEQ@UO7IUrGb8k$C~_o3Mn{lhWQRBwK43lEuav(7sti9-shmS7 z1n6&5PyM8F$qPoo0trn8E%8g%oSNXs&2lH=@SeFR2KMjFF3C=;9<^Yx-h#fl;o9d$ z1DHRHHr<7c;)EdLE#X$JVGCR6vG?<;ApUkHg{D&t0Cnr0@VSlm6=ZpM1Tc+`9^Knt zeMo`;gkWwiZ@-RYC=1%oih6z(iNgP$M&ZTz@=V7{9vO!oi)}vPVfT;osa^*LGSax* zmBe?K_!DkbDPulj9T%q(46iq(gpf3QPFr#HRN%?OA^MpY$^liZn_VARuG;NeVvVg~ z5(r8()j);t0pe&ogh9*S(KY_Th`jVE2P|nrpbj1if-PoOU@z8)CT3Krn7if@jBeJM zbrKRs@Lodv9i+fq^WpeHifeLPxX^_2o``~-RICHw&0Q8hNvC~f}ch1MV@?gQpnMQwhBjeRYgMk zPGHmQ{!QV;*9`L;fAx*N!BXO}DE4JFXS;5U&K>`i)gvOm1zC54=?~8ji}+DMn`2|e zHIO6U2vpo^U;RSH5&d_}|`feMw%;Jh@85IAxWP#ap5=b+uY zWN_j#I#2gKAI*M4-CzL)dAc(3Pn+?6-Ym9#uQ{ioE3%3PVg*SR3oo zl>yc>!^vnP!N?{@(Z=H?h#kwM2s8 z)HwaRq%4VQTsL)Os7Rs5nn7O2Iv92GL&%b=f}7lf0S|+`iTkROHz7wf8-W`6H1=Hd zDM+}N3K%)21*j#c)y!5TpPZ%#z+Dh=RYGdP93@O=A($$rDI%{{ob6ya{Y}s=_1@8J zk2^%jbXAVQcF%Ly?TfN)^fW%K5MD>ln7yGwOJ~ot>WxMyODutTGN_`@eb1%&vsX%Y z@yHJo4O(#pzFKLWlS3O^ELMU>LjihU*OuJG+Rn9(c*Tdrg3*+Eur1S zW3%k*L%d7E-d0}A6m1i^XS1UirHgb)yG2{eNWe3W{^$<5A*i?)oCbS+$eQ1&N@Pia z$&IT>O=iTLgV8BjF<}#(k$nx^V8-7Ra$m9!Zc>l<<9fo494|I{%Rjs!f_`2Fw@AKb zjEfspUMKo#uH1SJbalpq>BUO6dKSP6zd!thC?p7xq@#&eN)m+T2AXNJHc~WS%pm@V zVaG(RfN#|AzhTnf@?$X#x~VVidv}kzX-y}`JO_C)RqpNIST<(zVh1L?uRNyH1wz1? z2Z1x#%gS^fFNzr8Y&)1VK7_(2nEgb-uS#OqGTym|Mn`XnN4ASVSJ)}vlITq_CXu*U zNZZ)93ue}5cD6+tIEQ1TJzZRrzV>FQY=dJh*t(}(xbMravEsW$||t?!<-wKExFV{6>y#pP0n$!WZhwOQ3w)wKR1ULE@_Gk z3J0|0uUwh-J?WGeP8q5;2+OD#ota}p{Hlzit&YZ8KmI`HQ2Kk47lPdf-*dOSDgsG| zsuQkUeZ*Z&FHec-qehWWNR&&Q5X73W?bsnN343OLMSjIPMjgQ^_w<+1;;@~f$xbyd z5dcuAy@jo~F0<4IKq_xTum*JzABiXS(ySpdbqMmV+^Z_+o{p#OgzvyYrw}9>m1^th zuKbc9gU&$oy$byuv(^&`?s7_UZi+5*08!G%XR>SZ(nHy53GC&{5x+~Rv=@c^k#uK0 zP!Q|b)jV2Q%j!CzXr{Zmj&3WIVgr$<4YdSu8+rLfO>OOckW-I!AXWfUrI7@mmJsca zfC||mZBKm?>dca1-7FE}?1L1%q{AEH^w&WxVK6E>Gisuie;Kg7%xlS}mUp47*sGIy zrK$p_q$wnT?UdQ~~>h<Nwh5N#v}k!hSyS$+^-661z z@cCR;QVt1zTy|_od%ehSfTaNzh}BOtbq$CL)kmd+jmeyY<5V@wR0b$6o~g0sxS^JaIR(lciy}OJ{w@ z97b;Q)Ypwxo9A&I%=db%>Z8Fz;Bax3cgGBA$Ry;s?U^7t_gV)LJ{HK0BKQ$|I%HZ( ziDkw;xR13c|Ik7xEZFx^;J4`G_=&ZN=4Pj)T;S2sIxYU|g*M#!?+-v;uXD}hbI8IS z*DLfrT^^#&N)>f!r5K5FYa6_R6~WO4P2QrE*Ss)H6H<~lR&z1$Fpr9)*WqQlaPF@^ z3uZbS=z?whp;~gO69MnrplDw5s5<6hhcdZ17?K6bSxV;-%fNyrW}_L8V{zgH&G4pw zvJE6Lq~8S^d3<1mvUQ9Z7BTb*T(k*R{|liMUh73iF$B4JkM)$6+HU^Eg;$w+J&}AF zwftyh8`E}kqd2smZ1)kInfU1OB8P6IaV_T+Z$uRbShuAD;bc#i$Z>>Sy6 z`?h{bsXzihY35c1=N39?s3(jnyPC+`{EuSXyM5T$Cgw4N5sTB`kpPC1agjDmtMx`L zbaZ0JE=IZG&$`43>b(n*epU{4x(;sQzEua#edey1Ggbpxon^pSo;*=QvV_5x2LR+a z@a~6-7KdPSwCo(=MTD_S`Lm6ilWc>ENc530SB-psbNEsJ{KxD1TTBw*%bTu-&cW6qW? z;`n1?DS{x^Ff=zoVEzq*O~YrDx8#PhKSW2nIX=VEtK*i>g?@rC65S-gO*U2fJb`*l z7*si$Pd1uVm81>A4up_9Jk8t{1f#?CzZdQ!t2Y5P$FbI-hT<`Fq4r_f050Nq3QG?D z`DMy;a+tF*YRDym#Y*rof)xC>`l-8KqK0HDCOTC#RgBN!WZsh^uBR3@qFf`*C_krW z*+1nuuQ=*urJ9;+`zt`0ifs^mAX5Ai@d&)%`ip9$HvQBQh;+aUg((H*Oaf21{jpslT1Rh-{Z25X=x3y2QM5fQ2F)PqZd)vXA?Bx{>$#jd{1;0CG2c%Fak|4*VUC7Y z>x&ma^&%Wys>dgSLbgN!z?$q6ki-`3K^8$?pO&rA&mL8>tigt5VPchr&lcL94ekF2Qj#A39jTR+7=P zSP&mgbX-YT*GBBv7S|IcaDJfWu7TKPDrc+ndbS^W$ekvHf&TwsmeFFU0AZiX_0RRV zu}<5eX82zS{IL8$EfXOwL8O7g(U4JA@CqHrgN`%RQW3fkyvtj2%3g@*SNO9@i&tjA zpKHCfszoH5V7R~L)-QcPJv${tVAF}Y!JR`-1G>fgjkzJ1;OOSdKGD7h(b`q49w7uY z5?s{$D74AAWq|sbIBCz+lLIHu4_|Dbmm8G-ra7QxaQA_X^EOrkm~Xksg}gg9vL@0f z2t5aoFj=%bh1W|f&zr2o7kmlGg8N4Mru?`yeSG-u(NPk42V==pk#|B;zga%%bhgHL z#`BHMy*X){HZUthNTti+ZbbfCBVlyWy_U2x=%rS*mdim3UK%S?6~#A#%)k-{vv!Pj z+;lBu4XXxX5FtE%2k|igP10%=^npL6$UiGHBl%32O}Tf~OH5sT$r!s3zqqsN7D2pS zdY_ZWSZCA3s$4W@T84srwayxy;Tt(lnn|GpoC;3_tfkL80cr==c@9SI{I``-W)4ys z)Ngli5LEsDRt%A0GoaNCI7NDl8w^|@%Fh>|r0pqUKZe z5A|}zJlBzSmzSA_)1u{m{;gbhncpl@HmGFx(C?mTUo__4nPt801cL?#*8FoTzhKeE z-==#hXy^t)fC(W8xlXhdV6ebO?Q;PNulX~xcp)1bg3*x3g&26BlmjHB&{pD&?IFr( z(guBRiKE*fa(6^H2??r7oe_Y69SLrNUrjR*8Zl9#{1qvrCbMogxSyt>9uD2buj%%9 zE{IE-B;UHrG9+Y6OG5~6h+q$GQ7NyZToO$}Hv$la!wqH82hOf{>gP$g;h{hl=hdEQ zDU#Q^$-lvbaJg6j0BjO2*;v{|z{SIA7X#r1b%n94;w-?dNh^X_@Bj4U%Mhkvj%Wsf z2IUZ3AW^c6&e-2JQqBIy$K4i9y`(!H=AwzJI$K?dxo{fwEpy3d7tw?Yibk9^PTauB z`=h+X1y>$ZwfQuwp$YU=_>%_=BHcCRPMbKd)=QHeHR%hQ8X?*fEJ-V7P%0Lt6kcb) zD|>{wOYY(4DIyC4U~i>6h!i1z1a8oq+L8b0%~$r1 zY?Vdd5kqVTT`n1G>Krc1*Z1ASrSXx(k>)kpo|z5HHv46(E}q(=6e}Fg3$!mKBTJ-5 z(`!C@g16O~sV0;_;PPjKyFY0p{A|25 zMm>eyw1z|rU;ObP>#CNR?J$l{=OkiJ^ck?+=|ef3k*GPxtH`dsXu&0P|1MJmOnu#Z ziitomhDgE@Z(=jhJ(3=*PW+nR@MiKsu}<V^igxPtaI5~IJo{2zxqmLO1;vW!hXq@0*{2V}8)53%=W6@B%A;`(uGslY zdh^%{4wd_SGqPZdXkSG^CX$|@e;{9Jp2SV(%!tv2Sm1ZnQ?(bdJ8JEZb0HkTywg7% zzDoype35A+1Z1DPx2=Kyzuu(~X`;pAG&#Z*ufuK&7cRSR7H4dKzxZfNXYKWB5a)xM z5$wjC>b~BZoGTwSJ8p<|!JH^teV!Z4Cd-deG0@^weZA@LSZOOZ67?B)x839T_o&g; ziom{nWB*d4!>W1={}7ZA<r23Qp5JW1@PIh78(`O#JvaF6Y(hUe`k zz`kA&UE>&&%pR&840Dy0)HSc&Rhw6S?}M&EHBQuK4wr_2cCp zEf1J1h;*r|mD7kvb!jI68U+KDcW~!J3o?toZC!(D4#|N<-PO z)LzPvKG;1QiRVwJvD+BD=fw}tjY#jIvYjmn(zN%OA+}d9k9jWJF)Ar?Jq7z8Yfu?w z;9BkSmO_BLi4_8N_Z294ZFH=g#CD?@ta%DQK4Uq0hI6GoHxm9p)pQ&}S1ICrL<(^n z$9zFDDmLr~L{`-ct*JzWtlJf{JIJ|$f>Z+#!EyVL>y%}BO?=SXda9Z5&*0u!^VWiv zICckG>#tVzl7+MG!P)_?9}nQ-Fm(7<(w-0HRnR*jz#T5ebODcEM*Gh=e`0_Z z>-L&dX$s=-v%?4Kr$7vxxn^Ps;6`>oA=uqRI~-%A*|hP>4Ao(6S7W`e15}6*3NULD z4i*UU2q*y1rA0Sln8aA z>!jzW1Fevyn;G%G#)5n)IA2>NT!^HN?5Ug#V-nkv00?xG5e&yJJ9{r4mL{N@Gw>AX z>K>H!%(LCW{O;(7jQm1V*6C;g8pr@6^h>HjTUsV@PsfqGMj7m-+06Ediv_-BA4m=7 zsM8)<8Gs+Uoxze0Xa~{E8oZ;_FA-R2D0^iftbx>U8rgct9-|B}l|S}k7|5rCGEZOu zXc-r6M{gpcXA1!W8vvRRAd(o%E-Oh61$7tR(xX++vym7z*3E=#64h@a`R zcva5L(=^{9dM=g-2NtA8&XA8GS|R|d^^=C>d7p; z2Opv^@D4(=tk_bEITr#}F6jooO)4XT)Y~I@Hqg8=w5PyIO>h*!p)bg&=`D%M z%D9o%JO-sTr>MHB<-$}SwFL=h2;p=#EU zp-zP3P_Hj*+pp?E7ALKK_LM6#*b}h%UE%mohvB_y^+=(9g`jiL-w#XHT@DMfItB5C z@p5v3BU`6Z^=!4u67+lOO#nf@pq~B|+5yeW02{u6KPiAB(%xh($dNwXr2+2e-A?%> zYrkT3wNdVKJ=9Z@Kstk{19HVM>wp49hDC;qKX(L!6zsgho*Sj%pwDw1R3k5_|9Aic zDp1UF5CL`$>62L`v1T%L1xy>8lvhC1MP0~@n6Jcx@mwAN^^m>{zL%)gVWgLHncj9_ z(YQU63>NGj?{o5(TYRhy02>{>XQA3Su`7HB(nFvP61Pbs$`c$a+6}Y=H4kOfDBLC| zI0GPvDEOC=(`y^4{gT*;nz2tR2UD3496AxY!M=O@rtVk)Od5hfyZ7SPB0RUpwxGI#_NNlDNRB***d?`xzBR2Zq^5s59axkzqhEOCuSa={*H_qH{7`a zOf<@;#OLUqQ2qqFN@a&1(dWZLIldI`UI9>KIReW5jUItByC^zA^hOIRQO zg-$bn(C~GKrMU(@(uPLI1!;1=2$C$YaU17}sL_))NluZwO@_e3;18ftC?JdlEzvfk zdzg=o?b8grcL4fK8Z4(rSIMGT8l=mND2D4K!PNXWwX5ZO?T}7=dy702Yw| z{iqO#`<2ASN=wJ>zke?0-A?#1wcGjYI++#KTc<$?C4)>8W*pJB01HE@u-&7wl=Hth zDKS2&MmaiV=_mLJLfk4r{{UGZJ^--0q!~f{6~cThc|@;X-@SXEOuLgkbdzCuch8az z=TG)nMgS5lh>EqBp-v&Lkeo^g4S++q2Fdm{Ub^?ne94UIj--DiNVe{&K(`oIRu29@ z=I9xYt*BZ%+dASWG9hEKb9vfD3W zn|B#^MmQA)1^XPaHT6?-65}VdCRM}Nx*!O?$gm~^l(8)Ht+7fm;VWW*&Od@>j0Adt zxBvk;HX%m!z5tynKdwZ6@3;#kB#ABE?~%8fyLwveb8=T{xrSZr3d#y*4nb8&Ae4r5)&O&sNJ?zar$SF4vLd48hYX zsRN*3nHJcq*y+JI2!z4h8Z(qEV12`=81ktpVyqY$a@@5J@Oqk8&;d6Rv{|+EX9vaC z%`U9kwYcAS_Yft{YK|+q7k!tj!0gPm5)9-oknqR9j(GRTNJ%H}2C@##swA1Csd_Nv z!Q9g!qTMr|niM6`gS;Cwmf_zNytw+g9_@2|ZT!H;zJP$Up}GY9X=bGqdYpwYSWZ20 z;cY-@x2t~~@$Z!WLoY*-MjMY*4uLk;0~{!rcA6v6&N*3@Ty5hHW(PDK+DfK!hwk#K z2T-=KLChEY!3h_VR6nJS?+O*cK%Q+Q`qAc%wX?heJlFhqVtXzgyuXkci_E!Ev8jx; znbR(GYv_o{BSUaJN|}lr(@o%XfdL6~dUJxx81g>u4yYUTz`zPNb>+3NRiq<5xwiTl zPtF26&7*5S?NpVy6M3!iLQC zZ14(cMHz?+Bv0Vkn)tCY@We(1Ut9YAl@0-b;eM06JPp}0fJ2(O4Fjc1;o5)8gb>!a zo1`g;;FbUCD5qV}7yFr0br0IRap|uc=_V=o{l#+9yu#0xZ3k6&&h0R7oX~C!kx>JF zAQjZ6n~rHz30!_D!QDm?BqU#wO^kNk%>X2WGWQNH*Hb*0u$=s3;RiZwtxUZ!W@SH diff --git a/web/public/theme-mode/custom-mode.svg b/web/public/theme-mode/custom-mode.svg deleted file mode 100644 index 01be18d5b..000000000 --- a/web/public/theme-mode/custom-mode.svg +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/theme-mode/custom-theme-banner.svg b/web/public/theme-mode/custom-theme-banner.svg deleted file mode 100644 index b7434cd76..000000000 --- a/web/public/theme-mode/custom-theme-banner.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/theme-mode/dark-high-contrast.svg b/web/public/theme-mode/dark-high-contrast.svg deleted file mode 100644 index e5839b286..000000000 --- a/web/public/theme-mode/dark-high-contrast.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/theme-mode/dark-mode.svg b/web/public/theme-mode/dark-mode.svg deleted file mode 100644 index b8c14711c..000000000 --- a/web/public/theme-mode/dark-mode.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/theme-mode/light-high-contrast.svg b/web/public/theme-mode/light-high-contrast.svg deleted file mode 100644 index c2f5ded22..000000000 --- a/web/public/theme-mode/light-high-contrast.svg +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/theme-mode/light-mode.svg b/web/public/theme-mode/light-mode.svg deleted file mode 100644 index 9c7fdae4b..000000000 --- a/web/public/theme-mode/light-mode.svg +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/web-view-spinner.png b/web/public/web-view-spinner.png deleted file mode 100644 index 527f307c29f4f3ce814f7768090612a65f402123..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1456 zcmV;h1yA~kP)q;0MLRchA~g_{S)1UAU?!6ZxcvuWGI##3+c;HKUDs#?A%a9Ko6Vkfbaa$+xm=Qc z5^HN~g{P;d?>J^e=ZGp8LX*(~P$vb#GV=(KQgMjbm+I>3sw6T`l%RoTRxY5VClzh8 z#`*O&HZ}~Axk3aJiG%^={^Po2s2E{_fLZB*B(5tZIMq{-kcoH|h5X1irOI`c*4NiJ znIMD-LPZlQCb%vMfzy&C9wg6SJJx848iM(JKCrX1Q=o=(iPe+TaL~Cf1*VqV7GGIe zsRUPrtOnoHo#W=`=i{@pv!`=&bA_e^ySuw9krwIc=_xlN>QErS1&rxj-0%1EH#D$* zjd}|U3o?jNa?fHNneGd+|F{xt03ur4-Q7)+y3_#`X&XD_#whfI8{}+~CeG9FJ(<-8 zMh-4vJp@RFpqBRj{!Y0y#NQBo-}vjgy?A?jJB3M<5}X;|KuawyE^d-pF{WsbDItxF zIoF}og6O3&5){&5+A#Hl7%84&^|038+uPf0N{|4>Xi*A)MyaHP8;4EPw2A{Lsxuc5 z$;ZdX5cwUchOWF*F&pXU=jZI<;bEPdG_7W!W(W(CA4z;a$mf=pmQ>~e7Smp;#snqr(;K=2#;8fY)ifMjuEVnS{^hQO3j+UOBoYc$Z zGWlbrn$ZUa2CPa2{g%qXV9+FTRPn&1^d@K(7=~e(P)(tS$A6MzeTv+))9f`SMn$1E zAZMU23Swa*M;&;G6#Hp%OOgBgd!?_h?>k!g?(U8rfQVURNHr*E6&D~tbs&{zImdm- zzcVnoaKTE2rv_3%OsW>BH#avjOjZ4IK$VSsTImbG1!Rn)f}q5Wi`yn=Fr|5MaiQWN zg1BtDFFQ+qk~cv&MGJ#iU8FkR=FEqO2iHi%*aUYTuxrdJ$mc**0Z~#^LsJl#HJgT~ zuUogbw@Lc4z;!9`KT83a_>}z&s#t%2zeXp+!@~vMZJ>VS>gr0k8WMi(q`&|M?gRf*B}o#lD}@PSVR}!ID%T(@l&C3kIX@-Hco9>WAcP3o>hUGMHaIw_ zGeKYvCqFf~uB51nLIj-!Celorc>t-1Lv;=X4AM^f2-<3qndl=@Vsv!WU> Date: Fri, 8 Mar 2024 17:36:28 +0530 Subject: [PATCH 073/308] [WEB-665] fix Padding in the project dropdown. (#3905) --- web/components/project/sidebar-list.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/web/components/project/sidebar-list.tsx b/web/components/project/sidebar-list.tsx index 2cee91e6b..5744ee331 100644 --- a/web/components/project/sidebar-list.tsx +++ b/web/components/project/sidebar-list.tsx @@ -116,9 +116,14 @@ export const ProjectSidebarList: FC = observer(() => { )}
From f5151ae717de6d96a3bd2b7c4d68e5dc8cc97506 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Fri, 8 Mar 2024 17:37:01 +0530 Subject: [PATCH 074/308] [WEB-666] chore: rename `View profile` to `My activity`. (#3906) --- web/components/workspace/sidebar-dropdown.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/components/workspace/sidebar-dropdown.tsx b/web/components/workspace/sidebar-dropdown.tsx index 5d1695b33..5d07ff0f9 100644 --- a/web/components/workspace/sidebar-dropdown.tsx +++ b/web/components/workspace/sidebar-dropdown.tsx @@ -24,8 +24,8 @@ const userLinks = (workspaceSlug: string, userId: string) => [ icon: Mails, }, { - key: "view_profile", - name: "View profile", + key: "my_activity", + name: "My activity", href: `/${workspaceSlug}/profile/${userId}`, icon: CircleUserRound, }, @@ -38,7 +38,7 @@ const userLinks = (workspaceSlug: string, userId: string) => [ ]; const profileLinks = (workspaceSlug: string, userId: string) => [ { - name: "View profile", + name: "My activity", icon: UserCircle2, link: `/${workspaceSlug}/profile/${userId}`, }, From 2074bb97dbed9a55a65275cab0708e00deed013d Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Fri, 8 Mar 2024 17:38:42 +0530 Subject: [PATCH 075/308] [WEB-440] feat: create project feature selection modal. (#3909) * [WEB-440] feat: create project feature selection modal. * [WEB-399] chore: explain project identifier. * chore: use `Link` component for redirection to project page. --- .../project/create-project-form.tsx | 392 +++++++++++++++++ .../project/create-project-modal.tsx | 399 ++---------------- web/components/project/index.ts | 2 + .../project/project-feature-update.tsx | 57 +++ .../project/settings/features-list.tsx | 69 +-- .../[projectId]/settings/features.tsx | 12 +- 6 files changed, 535 insertions(+), 396 deletions(-) create mode 100644 web/components/project/create-project-form.tsx create mode 100644 web/components/project/project-feature-update.tsx diff --git a/web/components/project/create-project-form.tsx b/web/components/project/create-project-form.tsx new file mode 100644 index 000000000..509cf310c --- /dev/null +++ b/web/components/project/create-project-form.tsx @@ -0,0 +1,392 @@ +import { useState, FC, ChangeEvent } from "react"; +import { observer } from "mobx-react-lite"; +import { useForm, Controller } from "react-hook-form"; +import { Info, X } from "lucide-react"; +// ui +import { + Button, + CustomEmojiIconPicker, + CustomSelect, + EmojiIconPickerTypes, + Input, + setToast, + TextArea, + TOAST_TYPE, + Tooltip, +} from "@plane/ui"; +// components +import { ImagePickerPopover } from "components/core"; +import { MemberDropdown } from "components/dropdowns"; +import { ProjectLogo } from "./project-logo"; +// constants +import { PROJECT_CREATED } from "constants/event-tracker"; +import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "constants/project"; +// helpers +import { convertHexEmojiToDecimal, getRandomEmoji } from "helpers/emoji.helper"; +import { cn } from "helpers/common.helper"; +import { projectIdentifierSanitizer } from "helpers/project.helper"; +// hooks +import { useEventTracker, useProject } from "hooks/store"; +// types +import { IProject } from "@plane/types"; + +type Props = { + setToFavorite?: boolean; + workspaceSlug: string; + onClose: () => void; + handleNextStep: (projectId: string) => void; +}; + +const defaultValues: Partial = { + cover_image: PROJECT_UNSPLASH_COVERS[Math.floor(Math.random() * PROJECT_UNSPLASH_COVERS.length)], + description: "", + logo_props: { + in_use: "emoji", + emoji: { + value: getRandomEmoji(), + }, + }, + identifier: "", + name: "", + network: 2, + project_lead: null, +}; + +export const CreateProjectForm: FC = observer((props) => { + const { setToFavorite, workspaceSlug, onClose, handleNextStep } = props; + // store + const { captureProjectEvent } = useEventTracker(); + const { addProjectToFavorites, createProject } = useProject(); + // states + const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true); + // form info + const { + formState: { errors, isSubmitting }, + handleSubmit, + reset, + control, + watch, + setValue, + } = useForm({ + defaultValues, + reValidateMode: "onChange", + }); + + const handleAddToFavorites = (projectId: string) => { + if (!workspaceSlug) return; + + addProjectToFavorites(workspaceSlug.toString(), projectId).catch(() => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Couldn't remove the project from favorites. Please try again.", + }); + }); + }; + + const onSubmit = async (formData: Partial) => { + // Upper case identifier + formData.identifier = formData.identifier?.toUpperCase(); + + return createProject(workspaceSlug.toString(), formData) + .then((res) => { + const newPayload = { + ...res, + state: "SUCCESS", + }; + captureProjectEvent({ + eventName: PROJECT_CREATED, + payload: newPayload, + }); + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Project created successfully.", + }); + if (setToFavorite) { + handleAddToFavorites(res.id); + } + handleNextStep(res.id); + }) + .catch((err) => { + Object.keys(err.data).map((key) => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: err.data[key], + }); + captureProjectEvent({ + eventName: PROJECT_CREATED, + payload: { + ...formData, + state: "FAILED", + }, + }); + }); + }); + }; + + const handleNameChange = (onChange: any) => (e: ChangeEvent) => { + if (!isChangeInIdentifierRequired) { + onChange(e); + return; + } + if (e.target.value === "") setValue("identifier", ""); + else setValue("identifier", projectIdentifierSanitizer(e.target.value).substring(0, 5)); + onChange(e); + }; + + const handleIdentifierChange = (onChange: any) => (e: ChangeEvent) => { + const { value } = e.target; + const alphanumericValue = projectIdentifierSanitizer(value); + setIsChangeInIdentifierRequired(false); + onChange(alphanumericValue); + }; + + const handleClose = () => { + onClose(); + setIsChangeInIdentifierRequired(true); + setTimeout(() => { + reset(); + }, 300); + }; + + return ( + <> +
+ {watch("cover_image") && ( + Cover image + )} + +
+ +
+
+ ( + + )} + /> +
+
+ ( + + + + } + onChange={(val: any) => { + let logoValue = {}; + + if (val.type === "emoji") + logoValue = { + value: convertHexEmojiToDecimal(val.value.unified), + url: val.value.imageUrl, + }; + else if (val.type === "icon") logoValue = val.value; + + onChange({ + in_use: val.type, + [val.type]: logoValue, + }); + }} + defaultIconColor={value.in_use === "icon" ? value.icon?.color : undefined} + defaultOpen={value.in_use === "emoji" ? EmojiIconPickerTypes.EMOJI : EmojiIconPickerTypes.ICON} + /> + )} + /> +
+
+
+
+
+
+ ( + + )} + /> + + <>{errors?.name?.message} + +
+
+ + /^[ÇŞĞIİÖÜA-Z0-9]+$/.test(value.toUpperCase()) || + "Only Alphanumeric & Non-latin characters are allowed.", + minLength: { + value: 1, + message: "Project ID must at least be of 1 character", + }, + maxLength: { + value: 5, + message: "Project ID must at most be of 5 characters", + }, + }} + render={({ field: { value, onChange } }) => ( + + )} + /> + + + + {errors?.identifier?.message} +
+
+ ( +