mirror of
https://github.com/makeplane/plane.git
synced 2026-02-24 04:00:14 +01:00
fix: merge conflicts resolved
This commit is contained in:
38
web/components/active-cycles/card.tsx
Normal file
38
web/components/active-cycles/card.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { FC } from "react";
|
||||
// types
|
||||
import { IActiveCycle } from "@plane/types";
|
||||
// components
|
||||
import {
|
||||
ActiveCyclesProjectTitle,
|
||||
ActiveCycleHeader,
|
||||
ActiveCycleProgress,
|
||||
ActiveCycleProductivity,
|
||||
ActiveCycleStats,
|
||||
} from "@/components/active-cycles";
|
||||
|
||||
export type ActiveCycleInfoCardProps = {
|
||||
cycle: IActiveCycle;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const ActiveCycleInfoCard: FC<ActiveCycleInfoCardProps> = (props) => {
|
||||
const { cycle, workspaceSlug, projectId } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={cycle.id}
|
||||
className="flex flex-col gap-4 p-4 rounded-xl border border-custom-border-200 bg-custom-background-100"
|
||||
>
|
||||
<ActiveCyclesProjectTitle project={cycle.project_detail} />
|
||||
|
||||
<ActiveCycleHeader cycle={cycle} workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3">
|
||||
<ActiveCycleProgress cycle={cycle} />
|
||||
<ActiveCycleProductivity cycle={cycle} />
|
||||
<ActiveCycleStats cycle={cycle} workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
284
web/components/active-cycles/cycle-stats.tsx
Normal file
284
web/components/active-cycles/cycle-stats.tsx
Normal file
@@ -0,0 +1,284 @@
|
||||
import { FC, Fragment } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import useSWR from "swr";
|
||||
// icons
|
||||
import { CalendarCheck } from "lucide-react";
|
||||
// headless ui
|
||||
import { Tab } from "@headlessui/react";
|
||||
// types
|
||||
import { IActiveCycle } from "@plane/types";
|
||||
// ui
|
||||
import { Tooltip, Loader, PriorityIcon, Avatar } from "@plane/ui";
|
||||
// components
|
||||
import { SingleProgressStats } from "@/components/core";
|
||||
import { StateDropdown } from "@/components/dropdowns";
|
||||
// constants
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
import { CYCLE_ISSUES_WITH_PARAMS } from "@/constants/fetch-keys";
|
||||
import { EIssuesStoreType } from "@/constants/issue";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { renderFormattedDate, renderFormattedDateWithoutYear } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useIssues, useProjectState } from "@/hooks/store";
|
||||
import useLocalStorage from "@/hooks/use-local-storage";
|
||||
import { EmptyState } from "../empty-state";
|
||||
|
||||
export type ActiveCycleStatsProps = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
cycle: IActiveCycle;
|
||||
};
|
||||
|
||||
export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
||||
const { workspaceSlug, projectId, cycle } = props;
|
||||
|
||||
const { storedValue: tab, setValue: setTab } = useLocalStorage("activeCycleTab", "Assignees");
|
||||
|
||||
const currentValue = (tab: string | null) => {
|
||||
switch (tab) {
|
||||
case "Priority-Issues":
|
||||
return 0;
|
||||
case "Assignees":
|
||||
return 1;
|
||||
case "Labels":
|
||||
return 2;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
const {
|
||||
issues: { fetchActiveCycleIssues },
|
||||
} = useIssues(EIssuesStoreType.CYCLE);
|
||||
const { fetchWorkspaceStates } = useProjectState();
|
||||
|
||||
const { data: activeCycleIssues } = useSWR(
|
||||
workspaceSlug && projectId && cycle.id ? CYCLE_ISSUES_WITH_PARAMS(cycle.id, { priority: "urgent,high" }) : null,
|
||||
workspaceSlug && projectId && cycle.id ? () => fetchActiveCycleIssues(workspaceSlug, projectId, cycle.id) : null
|
||||
);
|
||||
|
||||
useSWR(
|
||||
workspaceSlug ? `WORKSPACE_STATES_${workspaceSlug}` : null,
|
||||
workspaceSlug ? () => fetchWorkspaceStates(workspaceSlug.toString()) : null
|
||||
);
|
||||
|
||||
const cycleIssues = activeCycleIssues ?? [];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 p-4 min-h-[17rem] overflow-hidden col-span-1 lg:col-span-2 xl:col-span-1 border border-custom-border-200 rounded-lg">
|
||||
<Tab.Group
|
||||
as={Fragment}
|
||||
defaultIndex={currentValue(tab)}
|
||||
onChange={(i) => {
|
||||
switch (i) {
|
||||
case 0:
|
||||
return setTab("Priority-Issues");
|
||||
case 1:
|
||||
return setTab("Assignees");
|
||||
case 2:
|
||||
return setTab("Labels");
|
||||
|
||||
default:
|
||||
return setTab("Priority-Issues");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tab.List
|
||||
as="div"
|
||||
className="relative border-[0.5px] border-custom-border-200 rounded bg-custom-background-80 p-[1px] grid"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(3, 1fr)`,
|
||||
}}
|
||||
>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
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-300 bg-custom-background-100": selected,
|
||||
"hover:text-custom-text-300": !selected,
|
||||
}
|
||||
)
|
||||
}
|
||||
>
|
||||
Priority Issues
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
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-300 bg-custom-background-100": selected,
|
||||
"hover:text-custom-text-300": !selected,
|
||||
}
|
||||
)
|
||||
}
|
||||
>
|
||||
Assignees
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
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-300 bg-custom-background-100": selected,
|
||||
"hover:text-custom-text-300": !selected,
|
||||
}
|
||||
)
|
||||
}
|
||||
>
|
||||
Labels
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
|
||||
<Tab.Panels as={Fragment}>
|
||||
<Tab.Panel
|
||||
as="div"
|
||||
className="flex h-52 w-full flex-col gap-1 overflow-y-auto text-custom-text-200 vertical-scrollbar scrollbar-sm"
|
||||
>
|
||||
<div className="flex flex-col gap-1 h-full w-full overflow-y-auto vertical-scrollbar scrollbar-sm">
|
||||
{cycleIssues ? (
|
||||
cycleIssues.length > 0 ? (
|
||||
cycleIssues.map((issue: any) => (
|
||||
<Link
|
||||
key={issue.id}
|
||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
|
||||
className="group flex cursor-pointer items-center justify-between gap-2 rounded-md hover:bg-custom-background-90 p-1"
|
||||
>
|
||||
<div className="flex items-center gap-1.5 flex-grow w-full min-w-24 truncate">
|
||||
<PriorityIcon priority={issue.priority} withContainer size={12} />
|
||||
|
||||
<Tooltip
|
||||
tooltipHeading="Issue ID"
|
||||
tooltipContent={`${cycle.project_detail?.identifier}-${issue.sequence_id}`}
|
||||
>
|
||||
<span className="flex-shrink-0 text-xs text-custom-text-200">
|
||||
{cycle.project_detail?.identifier}-{issue.sequence_id}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
|
||||
<span className="text-[0.825rem] text-custom-text-100 truncate">{issue.name}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 flex-shrink-0">
|
||||
<StateDropdown
|
||||
value={issue.state_id ?? undefined}
|
||||
onChange={() => {}}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
disabled
|
||||
buttonVariant="background-with-text"
|
||||
buttonContainerClassName="cursor-pointer max-w-24"
|
||||
showTooltip
|
||||
/>
|
||||
{issue.target_date && (
|
||||
<Tooltip tooltipHeading="Target Date" tooltipContent={renderFormattedDate(issue.target_date)}>
|
||||
<div className="h-full flex truncate items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80 group-hover:bg-custom-background-100 cursor-pointer">
|
||||
<CalendarCheck className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="text-xs truncate">
|
||||
{renderFormattedDateWithoutYear(issue.target_date)}
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
))
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<EmptyState
|
||||
type={EmptyStateType.ACTIVE_CYCLE_PRIORITY_ISSUE_EMPTY_STATE}
|
||||
layout="screen-simple"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<Loader className="space-y-3">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
|
||||
<Tab.Panel
|
||||
as="div"
|
||||
className="flex h-52 w-full flex-col gap-1 overflow-y-auto text-custom-text-200 vertical-scrollbar scrollbar-sm"
|
||||
>
|
||||
{cycle?.distribution?.assignees && cycle.distribution.assignees.length > 0 ? (
|
||||
cycle.distribution.assignees.map((assignee, index) => {
|
||||
if (assignee.assignee_id)
|
||||
return (
|
||||
<SingleProgressStats
|
||||
key={assignee.assignee_id}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar name={assignee?.display_name ?? undefined} src={assignee?.avatar ?? undefined} />
|
||||
|
||||
<span>{assignee.display_name}</span>
|
||||
</div>
|
||||
}
|
||||
completed={assignee.completed_issues}
|
||||
total={assignee.total_issues}
|
||||
/>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<SingleProgressStats
|
||||
key={`unassigned-${index}`}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-5 w-5 rounded-full border-2 border-custom-border-200 bg-custom-background-80">
|
||||
<img src="/user.png" height="100%" width="100%" className="rounded-full" alt="User" />
|
||||
</div>
|
||||
<span>No assignee</span>
|
||||
</div>
|
||||
}
|
||||
completed={assignee.completed_issues}
|
||||
total={assignee.total_issues}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<EmptyState type={EmptyStateType.ACTIVE_CYCLE_ASSIGNEE_EMPTY_STATE} layout="screen-simple" size="sm" />
|
||||
</div>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
|
||||
<Tab.Panel
|
||||
as="div"
|
||||
className="flex h-52 w-full flex-col gap-1 overflow-y-auto text-custom-text-200 vertical-scrollbar scrollbar-sm"
|
||||
>
|
||||
{cycle?.distribution?.labels && cycle.distribution.labels.length > 0 ? (
|
||||
cycle.distribution.labels.map((label, index) => (
|
||||
<SingleProgressStats
|
||||
key={label.label_id ?? `no-label-${index}`}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="block h-3 w-3 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label.color ?? "#000000",
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs">{label.label_name ?? "No labels"}</span>
|
||||
</div>
|
||||
}
|
||||
completed={label.completed_issues}
|
||||
total={label.total_issues}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<EmptyState type={EmptyStateType.ACTIVE_CYCLE_LABEL_EMPTY_STATE} layout="screen-simple" size="sm" />
|
||||
</div>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
77
web/components/active-cycles/header.tsx
Normal file
77
web/components/active-cycles/header.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { FC } from "react";
|
||||
import Link from "next/link";
|
||||
// types
|
||||
import { ICycle, TCycleGroups } from "@plane/types";
|
||||
// ui
|
||||
import { Tooltip, CycleGroupIcon, getButtonStyling, Avatar, AvatarGroup } from "@plane/ui";
|
||||
// helpers
|
||||
import { renderFormattedDate, findHowManyDaysLeft } from "@/helpers/date-time.helper";
|
||||
import { truncateText } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useMember } from "@/hooks/store";
|
||||
|
||||
export type ActiveCycleHeaderProps = {
|
||||
cycle: ICycle;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const ActiveCycleHeader: FC<ActiveCycleHeaderProps> = (props) => {
|
||||
const { cycle, workspaceSlug, projectId } = props;
|
||||
// store
|
||||
const { getUserDetails } = useMember();
|
||||
const cycleOwnerDetails = cycle && cycle.owned_by_id ? getUserDetails(cycle.owned_by_id) : undefined;
|
||||
|
||||
const daysLeft = findHowManyDaysLeft(cycle.end_date) ?? 0;
|
||||
const currentCycleStatus = cycle?.status?.toLocaleLowerCase() as TCycleGroups;
|
||||
|
||||
const cycleAssignee = (cycle.distribution?.assignees ?? []).filter((assignee) => assignee.display_name);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between px-3 py-1.5 rounded border-[0.5px] border-custom-border-100 bg-custom-background-90">
|
||||
<div className="flex items-center gap-2 cursor-default">
|
||||
<CycleGroupIcon cycleGroup={currentCycleStatus} className="h-4 w-4" />
|
||||
<Tooltip tooltipContent={cycle.name} position="top-left">
|
||||
<h3 className="break-words text-lg font-medium">{truncateText(cycle.name, 70)}</h3>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
tooltipContent={`Start date: ${renderFormattedDate(cycle.start_date ?? "")} Due Date: ${renderFormattedDate(
|
||||
cycle.end_date ?? ""
|
||||
)}`}
|
||||
position="top-left"
|
||||
>
|
||||
<span className="flex gap-1 whitespace-nowrap rounded-sm text-custom-text-400 font-semibold text-sm leading-5">
|
||||
{`${daysLeft} ${daysLeft > 1 ? "days" : "day"} left`}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="rounded-sm text-sm">
|
||||
<div className="flex gap-2 divide-x spac divide-x-border-300 text-sm whitespace-nowrap text-custom-text-300 font-medium">
|
||||
<Avatar name={cycleOwnerDetails?.display_name} src={cycleOwnerDetails?.avatar} />
|
||||
{cycleAssignee.length > 0 && (
|
||||
<span className="pl-2">
|
||||
<AvatarGroup showTooltip>
|
||||
{cycleAssignee.map((member) => (
|
||||
<Avatar
|
||||
key={member.assignee_id}
|
||||
name={member?.display_name ?? ""}
|
||||
src={member?.avatar ?? ""}
|
||||
showTooltip={false}
|
||||
/>
|
||||
))}
|
||||
</AvatarGroup>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}
|
||||
className={`${getButtonStyling("outline-primary", "sm")} cursor-pointer`}
|
||||
>
|
||||
View Cycle
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
7
web/components/active-cycles/index.ts
Normal file
7
web/components/active-cycles/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from "./header";
|
||||
export * from "./progress";
|
||||
export * from "./project-title";
|
||||
export * from "./productivity";
|
||||
export * from "./cycle-stats";
|
||||
export * from "./card";
|
||||
export * from "./list-page";
|
||||
54
web/components/active-cycles/list-page.tsx
Normal file
54
web/components/active-cycles/list-page.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { FC, useEffect } from "react";
|
||||
import useSWR from "swr";
|
||||
// ui
|
||||
import { Spinner } from "@plane/ui";
|
||||
// components
|
||||
import { ActiveCycleInfoCard } from "@/components/active-cycles";
|
||||
// constants
|
||||
import { WORKSPACE_ACTIVE_CYCLES_LIST } from "@/constants/fetch-keys";
|
||||
// services
|
||||
import { CycleService } from "@/services/cycle.service";
|
||||
const cycleService = new CycleService();
|
||||
|
||||
export type ActiveCyclesListPageProps = {
|
||||
workspaceSlug: string;
|
||||
cursor: string;
|
||||
perPage: number;
|
||||
updateTotalPages: (count: number) => void;
|
||||
updateResultsCount: (count: number) => void;
|
||||
};
|
||||
|
||||
export const ActiveCyclesListPage: FC<ActiveCyclesListPageProps> = (props) => {
|
||||
const { workspaceSlug, cursor, perPage, updateTotalPages, updateResultsCount } = props;
|
||||
|
||||
// fetching active cycles in workspace
|
||||
const { data: workspaceActiveCycles } = useSWR(
|
||||
workspaceSlug && cursor ? WORKSPACE_ACTIVE_CYCLES_LIST(workspaceSlug as string, cursor, `${perPage}`) : null,
|
||||
workspaceSlug && cursor ? () => cycleService.workspaceActiveCycles(workspaceSlug.toString(), cursor, perPage) : null
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspaceActiveCycles) {
|
||||
updateTotalPages(workspaceActiveCycles.total_pages);
|
||||
updateResultsCount(workspaceActiveCycles.results.length);
|
||||
}
|
||||
}, [updateTotalPages, updateResultsCount, workspaceActiveCycles]);
|
||||
|
||||
if (!workspaceActiveCycles) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{workspaceActiveCycles.results.map((cycle: any) => (
|
||||
<div key={cycle.id} className="px-5 pt-5 last:pb-5">
|
||||
<ActiveCycleInfoCard workspaceSlug={workspaceSlug?.toString()} projectId={cycle.project_id} cycle={cycle} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
57
web/components/active-cycles/productivity.tsx
Normal file
57
web/components/active-cycles/productivity.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { FC } from "react";
|
||||
// types
|
||||
import { ICycle } from "@plane/types";
|
||||
// components
|
||||
import ProgressChart from "@/components/core/sidebar/progress-chart";
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
import { EmptyState } from "../empty-state";
|
||||
|
||||
export type ActiveCycleProductivityProps = {
|
||||
cycle: ICycle;
|
||||
};
|
||||
|
||||
export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = (props) => {
|
||||
const { cycle } = props;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center min-h-[17rem] gap-5 py-4 px-3.5 border border-custom-border-200 rounded-lg">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<h3 className="text-base text-custom-text-300 font-semibold">Issue burndown</h3>
|
||||
</div>
|
||||
{cycle.total_issues > 0 ? (
|
||||
<>
|
||||
<div className="h-full w-full px-2">
|
||||
<div className="flex items-center justify-between gap-4 py-1 text-xs text-custom-text-300">
|
||||
<div className="flex items-center gap-3 text-custom-text-300">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<span className="h-2 w-2 rounded-full bg-[#A9BBD0]" />
|
||||
<span>Ideal</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<span className="h-2 w-2 rounded-full bg-[#4C8FFF]" />
|
||||
<span>Current</span>
|
||||
</div>
|
||||
</div>
|
||||
<span>{`Pending issues - ${cycle.backlog_issues + cycle.unstarted_issues + cycle.started_issues}`}</span>
|
||||
</div>
|
||||
<div className="relative h-full">
|
||||
<ProgressChart
|
||||
className="h-full"
|
||||
distribution={cycle.distribution?.completion_chart ?? {}}
|
||||
startDate={cycle.start_date ?? ""}
|
||||
endDate={cycle.end_date ?? ""}
|
||||
totalIssues={cycle.total_issues}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<EmptyState type={EmptyStateType.ACTIVE_CYCLE_CHART_EMPTY_STATE} layout="screen-simple" size="sm" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
89
web/components/active-cycles/progress.tsx
Normal file
89
web/components/active-cycles/progress.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { FC } from "react";
|
||||
// types
|
||||
import { ICycle } from "@plane/types";
|
||||
// ui
|
||||
import { LinearProgressIndicator } from "@plane/ui";
|
||||
// constants
|
||||
import { WORKSPACE_ACTIVE_CYCLE_STATE_GROUPS_DETAILS } from "@/constants/cycle";
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
import { EmptyState } from "../empty-state";
|
||||
|
||||
export type ActiveCycleProgressProps = {
|
||||
cycle: ICycle;
|
||||
};
|
||||
|
||||
export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = (props) => {
|
||||
const { cycle } = props;
|
||||
|
||||
const progressIndicatorData = WORKSPACE_ACTIVE_CYCLE_STATE_GROUPS_DETAILS.map((group, index) => ({
|
||||
id: index,
|
||||
name: group.title,
|
||||
value: cycle.total_issues > 0 ? (cycle[group.key as keyof ICycle] as number) : 0,
|
||||
color: group.color,
|
||||
}));
|
||||
|
||||
const groupedIssues: any = {
|
||||
completed: cycle.completed_issues,
|
||||
started: cycle.started_issues,
|
||||
unstarted: cycle.unstarted_issues,
|
||||
backlog: cycle.backlog_issues,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-[17rem] gap-5 py-4 px-3.5 border border-custom-border-200 rounded-lg">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<h3 className="text-base text-custom-text-300 font-semibold">Progress</h3>
|
||||
{cycle.total_issues > 0 && (
|
||||
<span className="flex gap-1 text-sm text-custom-text-400 font-medium whitespace-nowrap rounded-sm px-3 py-1 ">
|
||||
{`${cycle.completed_issues + cycle.cancelled_issues}/${cycle.total_issues - cycle.cancelled_issues} ${
|
||||
cycle.completed_issues + cycle.cancelled_issues > 1 ? "Issues" : "Issue"
|
||||
} closed`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{cycle.total_issues > 0 && <LinearProgressIndicator size="lg" data={progressIndicatorData} />}
|
||||
</div>
|
||||
|
||||
{cycle.total_issues > 0 ? (
|
||||
<div className="flex flex-col gap-5">
|
||||
{Object.keys(groupedIssues).map((group, index) => (
|
||||
<>
|
||||
{groupedIssues[group] > 0 && (
|
||||
<div key={index}>
|
||||
<div className="flex items-center justify-between gap-2 text-sm">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
className="block h-3 w-3 rounded-full"
|
||||
style={{
|
||||
backgroundColor: WORKSPACE_ACTIVE_CYCLE_STATE_GROUPS_DETAILS[index].color,
|
||||
}}
|
||||
/>
|
||||
<span className="text-custom-text-300 capitalize font-medium w-16">{group}</span>
|
||||
</div>
|
||||
<span className="text-custom-text-300">{`${groupedIssues[group]} ${
|
||||
groupedIssues[group] > 1 ? "Issues" : "Issue"
|
||||
}`}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
{cycle.cancelled_issues > 0 && (
|
||||
<span className="flex items-center gap-2 text-sm text-custom-text-300">
|
||||
<span>
|
||||
{`${cycle.cancelled_issues} cancelled ${
|
||||
cycle.cancelled_issues > 1 ? "issues are" : "issue is"
|
||||
} excluded from this report.`}{" "}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<EmptyState type={EmptyStateType.ACTIVE_CYCLE_PROGRESS_EMPTY_STATE} layout="screen-simple" size="sm" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
19
web/components/active-cycles/project-title.tsx
Normal file
19
web/components/active-cycles/project-title.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { FC } from "react";
|
||||
// types
|
||||
import { IProject } from "@plane/types";
|
||||
// ui
|
||||
import { ProjectLogo } from "../project";
|
||||
|
||||
export type ActiveCyclesProjectTitleProps = {
|
||||
project: Partial<IProject> | undefined;
|
||||
};
|
||||
|
||||
export const ActiveCyclesProjectTitle: FC<ActiveCyclesProjectTitleProps> = (props) => {
|
||||
const { project } = props;
|
||||
return (
|
||||
<div className="flex items-center gap-1.5">
|
||||
{project?.logo_props && <ProjectLogo logo={project.logo_props} />}
|
||||
<h2 className="text-xl font-semibold">{project?.name}</h2>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
44
web/components/cycles/active-cycle-info.tsx
Normal file
44
web/components/cycles/active-cycle-info.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// types
|
||||
import { IActiveCycle } from "@plane/types";
|
||||
// components
|
||||
import {
|
||||
ActiveCyclesProjectTitle,
|
||||
ActiveCycleHeader,
|
||||
ActiveCycleProgress,
|
||||
ActiveCycleProductivity,
|
||||
ActiveCycleStats,
|
||||
} from "@/components/active-cycles";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store";
|
||||
|
||||
export type ActiveCycleInfoProps = {
|
||||
cycle: IActiveCycle;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const ActiveCycleInfo: FC<ActiveCycleInfoProps> = observer((props) => {
|
||||
const { cycle, workspaceSlug, projectId } = props;
|
||||
|
||||
const { getProjectById } = useProject();
|
||||
|
||||
const projectDetails = getProjectById(projectId);
|
||||
|
||||
if (!projectDetails) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActiveCyclesProjectTitle project={projectDetails} />
|
||||
<div className="flex flex-col gap-2 rounded border border-custom-border-200">
|
||||
<ActiveCycleHeader cycle={cycle} workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
|
||||
<ActiveCycleProgress cycle={cycle} />
|
||||
<ActiveCycleProductivity cycle={cycle} />
|
||||
<ActiveCycleStats cycle={cycle} workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from "./cycles-view";
|
||||
export * from "./active-cycle-info";
|
||||
export * from "./active-cycle";
|
||||
export * from "./applied-filters";
|
||||
export * from "./board/";
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { observer } from "mobx-react";
|
||||
// ui
|
||||
import { Crown } from "lucide-react";
|
||||
import { Breadcrumbs, ContrastIcon } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
// icons
|
||||
|
||||
export const WorkspaceActiveCycleHeader = observer(() => (
|
||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||
@@ -20,7 +19,9 @@ export const WorkspaceActiveCycleHeader = observer(() => (
|
||||
}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<Crown className="h-3.5 w-3.5 text-amber-400" />
|
||||
<span className="flex items-center justify-center px-3.5 py-0.5 text-xs leading-4 rounded-xl text-orange-500 bg-orange-500/20">
|
||||
Beta
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
1
web/components/license/index.ts
Normal file
1
web/components/license/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./pro-plan-modal";
|
||||
192
web/components/license/pro-plan-modal.tsx
Normal file
192
web/components/license/pro-plan-modal.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
import { FC, Fragment, useState } from "react";
|
||||
// icons
|
||||
import { CheckCircle } from "lucide-react";
|
||||
// ui
|
||||
import { Dialog, Transition, Tab } from "@headlessui/react";
|
||||
// store
|
||||
import { useEventTracker } from "@/hooks/store";
|
||||
|
||||
function classNames(...classes: any[]) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
const PRICING_CATEGORIES = ["Monthly", "Yearly"];
|
||||
|
||||
const MONTHLY_PLAN_ITEMS = [
|
||||
"White-glove onboarding for your use-cases",
|
||||
"Bespoke implementation",
|
||||
"Priority integrations",
|
||||
"Priority Support and SLAs",
|
||||
"Early access to all paid features",
|
||||
"Locked-in discount for a whole year",
|
||||
];
|
||||
|
||||
const YEARLY_PLAN_ITEMS = [
|
||||
"White-glove onboarding for your use-cases",
|
||||
"Bespoke implementation",
|
||||
"Priority integrations",
|
||||
"Priority Support and SLAs",
|
||||
"Early access to all paid features",
|
||||
"Tiered discounts for the second and third years",
|
||||
];
|
||||
|
||||
export type ProPlanModalProps = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
export const ProPlanModal: FC<ProPlanModalProps> = (props) => {
|
||||
const { isOpen, handleClose } = props;
|
||||
// store
|
||||
const { captureEvent } = useEventTracker();
|
||||
// states
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
|
||||
const handleProPlaneMonthRedirection = () => {
|
||||
if (process.env.NEXT_PUBLIC_PRO_PLAN_MONTHLY_REDIRECT_URL) {
|
||||
window.open(process.env.NEXT_PUBLIC_PRO_PLAN_MONTHLY_REDIRECT_URL, "_blank");
|
||||
captureEvent("pro_plan_modal_month_redirection", {});
|
||||
}
|
||||
};
|
||||
|
||||
const handleProPlanYearlyRedirection = () => {
|
||||
if (process.env.NEXT_PUBLIC_PRO_PLAN_YEARLY_REDIRECT_URL) {
|
||||
window.open(process.env.NEXT_PUBLIC_PRO_PLAN_YEARLY_REDIRECT_URL, "_blank");
|
||||
captureEvent("pro_plan_modal_yearly_redirection", {});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-50" onClose={handleClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-lg transform overflow-hidden rounded-2xl bg-custom-background-100 p-6 text-left align-middle shadow-xl transition-all border-[0.5px] border-custom-border-100">
|
||||
<Dialog.Title as="h2" className="text-2xl font-bold leading-6 mt-4 flex justify-center items-center">
|
||||
Early-adopter pricing for believers
|
||||
</Dialog.Title>
|
||||
<div className="mt-2 mb-5">
|
||||
<p className="text-center text-sm mb-6 px-10 text-custom-text-200">
|
||||
Build Plane to your specs. You decide what we prioritize and build for everyone. Also get tailored
|
||||
onboarding + implementation and priority support.
|
||||
</p>
|
||||
<Tab.Group>
|
||||
<div className="flex w-full justify-center">
|
||||
<Tab.List className="flex space-x-1 rounded-xl bg-custom-background-80 p-1 w-[72%]">
|
||||
{PRICING_CATEGORIES.map((category, index) => (
|
||||
<Tab
|
||||
key={category}
|
||||
className={({ selected }) =>
|
||||
classNames(
|
||||
"w-full rounded-lg py-2 text-sm font-medium leading-5",
|
||||
"ring-white/60 ring-offset-2 ring-offset-custom-primary-90 focus:outline-none",
|
||||
selected
|
||||
? "bg-custom-background-100 text-custom-primary-100 shadow"
|
||||
: "hover:bg-custom-background-80 text-custom-text-300 hover:text-custom-text-100"
|
||||
)
|
||||
}
|
||||
onClick={() => setTabIndex(index)}
|
||||
>
|
||||
<>
|
||||
{category}
|
||||
{category === "Yearly" && (
|
||||
<span className="bg-custom-primary-100 text-white rounded-full px-2 py-1 ml-1 text-xs">
|
||||
-28%
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
</div>
|
||||
|
||||
<Tab.Panels className="mt-2">
|
||||
<Tab.Panel className={classNames("rounded-xl bg-custom-background-100 p-3")}>
|
||||
<p className="ml-4 text-4xl font-bold mb-2">
|
||||
$7
|
||||
<span className="text-sm ml-3 text-custom-text-300">/user/month</span>
|
||||
</p>
|
||||
<ul>
|
||||
{MONTHLY_PLAN_ITEMS.map((item) => (
|
||||
<li key={item} className="relative rounded-md p-3 flex">
|
||||
<p className="text-sm font-medium leading-5 flex items-center">
|
||||
<CheckCircle className="h-4 w-4 mr-4" />
|
||||
<span>{item}</span>
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex justify-center w-full">
|
||||
<div className="relative inline-flex group mt-8">
|
||||
<div className="absolute transition-all duration-1000 opacity-50 -inset-px bg-gradient-to-r from-[#44BCFF] via-[#FF44EC] to-[#FF675E] rounded-xl blur-lg group-hover:opacity-100 group-hover:-inset-1 group-hover:duration-200 animate-tilt" />
|
||||
<button
|
||||
type="button"
|
||||
className="relative inline-flex items-center justify-center px-8 py-4 text-sm font-medium border-custom-border-100 border-[1.5px] transition-all duration-200 bg-custom-background-100 rounded-xl focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-custom-border-200"
|
||||
onClick={handleProPlaneMonthRedirection}
|
||||
>
|
||||
Become Early Adopter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel className={classNames("rounded-xl bg-custom-background-100 p-3")}>
|
||||
<p className="ml-4 text-4xl font-bold mb-2">
|
||||
$5
|
||||
<span className="text-sm ml-3 text-custom-text-300">/user/month</span>
|
||||
</p>
|
||||
<ul>
|
||||
{YEARLY_PLAN_ITEMS.map((item) => (
|
||||
<li key={item} className="relative rounded-md p-3 flex">
|
||||
<p className="text-sm font-medium leading-5 flex items-center">
|
||||
<CheckCircle className="h-4 w-4 mr-4" />
|
||||
<span>{item}</span>
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex justify-center w-full">
|
||||
<div className="relative inline-flex group mt-8">
|
||||
<div className="absolute transition-all duration-1000 opacity-50 -inset-px bg-gradient-to-r from-[#44BCFF] via-[#FF44EC] to-[#FF675E] rounded-xl blur-lg group-hover:opacity-100 group-hover:-inset-1 group-hover:duration-200 animate-tilt" />
|
||||
<button
|
||||
type="button"
|
||||
className="relative inline-flex items-center justify-center px-8 py-4 text-sm font-medium border-custom-border-100 border-[1.5px] transition-all duration-200 bg-custom-background-100 rounded-xl focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-custom-border-200"
|
||||
onClick={handleProPlanYearlyRedirection}
|
||||
>
|
||||
Become Early Adopter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
@@ -13,11 +13,12 @@ import {
|
||||
// types
|
||||
import { IUserLite, TPage } from "@plane/types";
|
||||
// components
|
||||
import { PageContentBrowser, PageContentLoader, PageEditorTitle } from "@/components/pages";
|
||||
import { IssueEmbedCard, PageContentBrowser, PageEditorTitle, PageContentLoader } from "@/components/pages";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useMember, useMention, useUser, useWorkspace } from "@/hooks/store";
|
||||
import { useIssueEmbed } from "@/hooks/use-issue-embed";
|
||||
import { usePageFilters } from "@/hooks/use-page-filters";
|
||||
import useReloadConfirmations from "@/hooks/use-reload-confirmation";
|
||||
// services
|
||||
@@ -79,8 +80,11 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
||||
members: projectMemberDetails,
|
||||
user: currentUser ?? undefined,
|
||||
});
|
||||
|
||||
// page filters
|
||||
const { isFullWidth } = usePageFilters();
|
||||
// issue-embed
|
||||
const { fetchIssues } = useIssueEmbed(workspaceSlug?.toString() ?? "", projectId?.toString() ?? "");
|
||||
|
||||
const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting");
|
||||
|
||||
@@ -88,6 +92,11 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
||||
updateMarkings(description_html ?? "<p></p>");
|
||||
}, [description_html, updateMarkings]);
|
||||
|
||||
const handleIssueSearch = async (searchQuery: string) => {
|
||||
const response = await fetchIssues(searchQuery);
|
||||
return response;
|
||||
};
|
||||
|
||||
if (pageDescription === undefined) return <PageContentLoader />;
|
||||
|
||||
return (
|
||||
@@ -149,6 +158,24 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
||||
highlights: mentionHighlights,
|
||||
suggestions: mentionSuggestions,
|
||||
}}
|
||||
embedHandler={{
|
||||
issue: {
|
||||
searchCallback: async (query) =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(async () => {
|
||||
const response = await handleIssueSearch(query);
|
||||
resolve(response);
|
||||
}, 300);
|
||||
}),
|
||||
widgetCallback: (issueId) => (
|
||||
<IssueEmbedCard
|
||||
issueId={issueId}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
/>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -162,6 +189,17 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
||||
mentionHandler={{
|
||||
highlights: mentionHighlights,
|
||||
}}
|
||||
embedHandler={{
|
||||
issue: {
|
||||
widgetCallback: (issueId) => (
|
||||
<IssueEmbedCard
|
||||
issueId={issueId}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
/>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
1
web/components/pages/editor/embed/index.ts
Normal file
1
web/components/pages/editor/embed/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./issue-embed";
|
||||
104
web/components/pages/editor/embed/issue-embed.tsx
Normal file
104
web/components/pages/editor/embed/issue-embed.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
// types
|
||||
import { IIssueDisplayProperties } from "@plane/types";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { IssueProperties } from "@/components/issues/issue-layouts/properties/all-properties";
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_PROPERTIES } from "@/constants/issue";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// hooks
|
||||
import { useIssueDetail, useProject, useUser } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
issueId: string;
|
||||
projectId: string;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const IssueEmbedCard: React.FC<Props> = observer((props) => {
|
||||
const { issueId, projectId, workspaceSlug } = props;
|
||||
// states
|
||||
const [error, setError] = useState<any | null>(null);
|
||||
// store hooks
|
||||
const {
|
||||
membership: { currentWorkspaceAllProjectsRole },
|
||||
} = useUser();
|
||||
const { getProjectById } = useProject();
|
||||
const {
|
||||
setPeekIssue,
|
||||
issue: { fetchIssue, getIssueById, updateIssue },
|
||||
} = useIssueDetail();
|
||||
// derived values
|
||||
const projectRole = currentWorkspaceAllProjectsRole?.[projectId];
|
||||
const projectDetails = getProjectById(projectId);
|
||||
const issueDetails = getIssueById(issueId);
|
||||
// auth
|
||||
const isReadOnly = !!projectRole && projectRole < EUserProjectRoles.MEMBER;
|
||||
// issue display properties
|
||||
const displayProperties: IIssueDisplayProperties = {};
|
||||
ISSUE_DISPLAY_PROPERTIES.forEach((property) => {
|
||||
displayProperties[property.key] = true;
|
||||
});
|
||||
// fetch issue details if not available
|
||||
useEffect(() => {
|
||||
if (!issueDetails) {
|
||||
fetchIssue(workspaceSlug, projectId, issueId)
|
||||
.then(() => setError(null))
|
||||
.catch((error) => setError(error));
|
||||
}
|
||||
}, [fetchIssue, issueDetails, issueId, projectId, workspaceSlug]);
|
||||
|
||||
if (!issueDetails && !error)
|
||||
return (
|
||||
<div className="rounded-md p-3 my-2">
|
||||
<Loader className="px-6">
|
||||
<Loader.Item height="30px" />
|
||||
<div className="mt-3 space-y-2">
|
||||
<Loader.Item height="20px" width="70%" />
|
||||
<Loader.Item height="20px" width="60%" />
|
||||
</div>
|
||||
</Loader>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (error)
|
||||
return (
|
||||
<div className="flex items-center gap-3 rounded-md border-2 border-orange-500 bg-orange-500/10 text-orange-500 px-4 py-3 my-2 text-base">
|
||||
<AlertTriangle className="text-orange-500 size-8" />
|
||||
This Issue embed is not found in any project. It can no longer be updated or accessed from here.
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="issue-embed cursor-pointer space-y-2 rounded-md bg-custom-background-90 p-3 my-2"
|
||||
role="button"
|
||||
onClick={() =>
|
||||
setPeekIssue({
|
||||
issueId,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
})
|
||||
}
|
||||
>
|
||||
<h5 className="text-xs text-custom-text-300">
|
||||
{projectDetails?.identifier}-{issueDetails?.sequence_id}
|
||||
</h5>
|
||||
<h4 className="text-sm font-medium line-clamp-2 break-words">{issueDetails?.name}</h4>
|
||||
{issueDetails && (
|
||||
<IssueProperties
|
||||
className="flex flex-wrap items-center gap-2 whitespace-nowrap text-custom-text-300 pt-1.5"
|
||||
issue={issueDetails}
|
||||
displayProperties={displayProperties}
|
||||
activeLayout="Page issue embed"
|
||||
updateIssue={async (projectId, issueId, data) => await updateIssue(workspaceSlug, projectId, issueId, data)}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./embed";
|
||||
export * from "./header";
|
||||
export * from "./summary";
|
||||
export * from "./editor-body";
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
// headless ui
|
||||
import { FileText, HelpCircle, MessagesSquare, MoveLeft, Zap } from "lucide-react";
|
||||
import { Transition } from "@headlessui/react";
|
||||
// icons
|
||||
import { FileText, HelpCircle, MessagesSquare, MoveLeft, Zap } from "lucide-react";
|
||||
// headless ui
|
||||
import { Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
|
||||
// hooks
|
||||
@@ -13,6 +13,7 @@ import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// assets
|
||||
import packageJson from "package.json";
|
||||
import { PlaneBadge } from "./plane-badge";
|
||||
|
||||
const HELP_OPTIONS = [
|
||||
{
|
||||
@@ -64,11 +65,9 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observe
|
||||
}`}
|
||||
>
|
||||
{!isCollapsed && (
|
||||
<Tooltip tooltipContent={`Version: v${packageJson.version}`} isMobile={isMobile}>
|
||||
<div className="w-1/2 cursor-default rounded-md bg-green-500/10 px-2 py-1 text-center text-xs font-medium text-green-500 outline-none leading-6">
|
||||
Community
|
||||
</div>
|
||||
</Tooltip>
|
||||
<>
|
||||
<PlaneBadge />
|
||||
</>
|
||||
)}
|
||||
<div className={`flex items-center gap-1 ${isCollapsed ? "flex-col justify-center" : "w-1/2 justify-evenly"}`}>
|
||||
<Tooltip tooltipContent="Shortcuts" isMobile={isMobile}>
|
||||
|
||||
@@ -9,3 +9,6 @@ export * from "./sidebar-dropdown";
|
||||
export * from "./sidebar-menu";
|
||||
export * from "./sidebar-quick-action";
|
||||
export * from "./workspace-active-cycles-upgrade";
|
||||
|
||||
// ee imports
|
||||
export * from "./workspace-active-cycles-list";
|
||||
|
||||
41
web/components/workspace/plane-badge.tsx
Normal file
41
web/components/workspace/plane-badge.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React, { useState } from "react";
|
||||
// ui
|
||||
import { Tooltip, Button } from "@plane/ui";
|
||||
// components
|
||||
import { ProPlanModal } from "@/components/license";
|
||||
// hooks
|
||||
import { useEventTracker } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// assets
|
||||
import packageJson from "package.json";
|
||||
|
||||
export const PlaneBadge: React.FC = () => {
|
||||
// states
|
||||
const [isProPlanModalOpen, setIsProPlanModalOpen] = useState(false);
|
||||
// hooks
|
||||
const { captureEvent } = useEventTracker();
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
const handleProPlanModalOpen = () => {
|
||||
setIsProPlanModalOpen(true);
|
||||
captureEvent("pro_plan_modal_opened", {});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProPlanModal isOpen={isProPlanModalOpen} handleClose={() => setIsProPlanModalOpen(false)} />
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
className="w-1/2 cursor-pointer rounded-2xl px-2.5 py-1.5 text-center text-sm font-medium outline-none"
|
||||
onClick={handleProPlanModalOpen}
|
||||
>
|
||||
Plane Pro
|
||||
</Button>
|
||||
<Tooltip tooltipContent={`Version: v${packageJson.version}`} isMobile={isMobile}>
|
||||
<div className="w-1/2 cursor-default rounded-md bg-green-500/10 px-2 py-1 text-center text-xs font-medium text-green-500 outline-none leading-6">
|
||||
Community
|
||||
</div>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -2,7 +2,6 @@ import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { Crown } from "lucide-react";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// components
|
||||
@@ -70,7 +69,9 @@ export const WorkspaceSidebarMenu = observer(() => {
|
||||
}
|
||||
{!sidebarCollapsed && <p className="leading-5">{link.label}</p>}
|
||||
{!sidebarCollapsed && link.key === "active-cycles" && (
|
||||
<Crown className="h-3.5 w-3.5 text-amber-400" />
|
||||
<span className="flex items-center justify-center px-3.5 py-0.5 text-xs leading-4 rounded-xl text-orange-500 bg-orange-500/20">
|
||||
Beta
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
66
web/components/workspace/workspace-active-cycles-list.tsx
Normal file
66
web/components/workspace/workspace-active-cycles-list.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { Button } from "@plane/ui";
|
||||
import { ActiveCyclesListPage } from "@/components/active-cycles";
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
import { EmptyState } from "../empty-state";
|
||||
|
||||
const perPage = 3;
|
||||
|
||||
export const WorkspaceActiveCyclesList = observer(() => {
|
||||
// state
|
||||
const [pageCount, setPageCount] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
const [resultsCount, setResultsCount] = useState(0); // workspaceActiveCycles.results.length
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const activeCyclesPages = [];
|
||||
|
||||
const updateTotalPages = (count: number) => {
|
||||
setTotalPages(count);
|
||||
};
|
||||
|
||||
const updateResultsCount = (count: number) => {
|
||||
setResultsCount(count);
|
||||
};
|
||||
|
||||
const handleLoadMore = () => {
|
||||
setPageCount(pageCount + 1);
|
||||
};
|
||||
|
||||
if (!workspaceSlug) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (let i = 1; i <= pageCount; i++) {
|
||||
activeCyclesPages.push(
|
||||
<ActiveCyclesListPage
|
||||
cursor={`${perPage}:${i - 1}:0`}
|
||||
perPage={perPage}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
updateTotalPages={updateTotalPages}
|
||||
updateResultsCount={updateResultsCount}
|
||||
key={i}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full w-full overflow-y-scroll bg-custom-background-90 vertical-scrollbar scrollbar-md">
|
||||
{activeCyclesPages}
|
||||
|
||||
{pageCount < totalPages && resultsCount !== 0 && (
|
||||
<div className="flex items-center justify-center gap-4 text-xs w-full py-5">
|
||||
<Button variant="outline-primary" size="sm" onClick={handleLoadMore}>
|
||||
Load More
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{resultsCount === 0 && <EmptyState type={EmptyStateType.WORKSPACE_ACTIVE_CYCLES} />}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -150,3 +150,27 @@ export const WORKSPACE_ACTIVE_CYCLES_DETAILS = [
|
||||
icon: Microscope,
|
||||
},
|
||||
];
|
||||
|
||||
// ee
|
||||
export const WORKSPACE_ACTIVE_CYCLE_STATE_GROUPS_DETAILS = [
|
||||
{
|
||||
key: "completed_issues",
|
||||
title: "Completed",
|
||||
color: "#6490FE",
|
||||
},
|
||||
{
|
||||
key: "started_issues",
|
||||
title: "Started",
|
||||
color: "#FDD97F",
|
||||
},
|
||||
{
|
||||
key: "unstarted_issues",
|
||||
title: "Unstarted",
|
||||
color: "#FEB055",
|
||||
},
|
||||
{
|
||||
key: "backlog_issues",
|
||||
title: "Backlog",
|
||||
color: "#F0F0F3",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -88,6 +88,7 @@ export enum EmptyStateType {
|
||||
ACTIVE_CYCLE_ASSIGNEE_EMPTY_STATE = "active-cycle-assignee-empty-state",
|
||||
ACTIVE_CYCLE_LABEL_EMPTY_STATE = "active-cycle-label-empty-state",
|
||||
|
||||
WORKSPACE_ACTIVE_CYCLES = "workspace-active-cycles",
|
||||
DISABLED_PROJECT_INBOX = "disabled-project-inbox",
|
||||
DISABLED_PROJECT_CYCLE = "disabled-project-cycle",
|
||||
DISABLED_PROJECT_MODULE = "disabled-project-module",
|
||||
@@ -608,6 +609,13 @@ const emptyStateDetails = {
|
||||
title: "Add labels to issues to see the \n breakdown of work by labels.",
|
||||
path: "/empty-state/active-cycle/label",
|
||||
},
|
||||
[EmptyStateType.WORKSPACE_ACTIVE_CYCLES]: {
|
||||
key: EmptyStateType.WORKSPACE_ACTIVE_CYCLES,
|
||||
title: "No active cycles",
|
||||
description:
|
||||
"Cycles of your projects that includes any period that encompasses today's date within its range. Find the progress and details of all your active cycle here.",
|
||||
path: "/empty-state/onboarding/workspace-active-cycles",
|
||||
},
|
||||
[EmptyStateType.DISABLED_PROJECT_INBOX]: {
|
||||
key: EmptyStateType.DISABLED_PROJECT_INBOX,
|
||||
title: "Inbox is not enabled for the project.",
|
||||
|
||||
@@ -140,6 +140,8 @@ export const WORKSPACE_LABELS = (workspaceSlug: string) => `WORKSPACE_LABELS_${w
|
||||
export const PROJECT_GITHUB_REPOSITORY = (projectId: string) => `PROJECT_GITHUB_REPOSITORY_${projectId.toUpperCase()}`;
|
||||
|
||||
// cycles
|
||||
export const WORKSPACE_ACTIVE_CYCLES_LIST = (workspaceSlug: string, cursor: string, per_page: string) =>
|
||||
`WORKSPACE_ACTIVE_CYCLES_LIST_${workspaceSlug.toUpperCase()}_${cursor.toUpperCase()}_${per_page.toUpperCase()}`;
|
||||
export const CYCLES_LIST = (projectId: string) => `CYCLE_LIST_${projectId.toUpperCase()}`;
|
||||
export const INCOMPLETE_CYCLES_LIST = (projectId: string) => `INCOMPLETE_CYCLES_LIST_${projectId.toUpperCase()}`;
|
||||
export const CURRENT_CYCLE_LIST = (projectId: string) => `CURRENT_CYCLE_LIST_${projectId.toUpperCase()}`;
|
||||
|
||||
@@ -115,6 +115,14 @@ export const PROJECT_SETTINGS_LINKS: {
|
||||
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/labels`,
|
||||
Icon: SettingIcon,
|
||||
},
|
||||
{
|
||||
key: "integrations",
|
||||
label: "Integrations",
|
||||
href: `/settings/integrations`,
|
||||
access: EUserProjectRoles.ADMIN,
|
||||
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/integrations`,
|
||||
Icon: SettingIcon,
|
||||
},
|
||||
{
|
||||
key: "estimates",
|
||||
label: "Estimates",
|
||||
|
||||
@@ -168,6 +168,22 @@ export const WORKSPACE_SETTINGS_LINKS: {
|
||||
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/billing`,
|
||||
Icon: SettingIcon,
|
||||
},
|
||||
{
|
||||
key: "integrations",
|
||||
label: "Integrations",
|
||||
href: `/settings/integrations`,
|
||||
access: EUserWorkspaceRoles.ADMIN,
|
||||
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/integrations`,
|
||||
Icon: SettingIcon,
|
||||
},
|
||||
{
|
||||
key: "import",
|
||||
label: "Imports",
|
||||
href: `/settings/imports`,
|
||||
access: EUserWorkspaceRoles.ADMIN,
|
||||
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/imports`,
|
||||
Icon: SettingIcon,
|
||||
},
|
||||
{
|
||||
key: "export",
|
||||
label: "Exports",
|
||||
|
||||
37
web/hooks/use-issue-embed.tsx
Normal file
37
web/hooks/use-issue-embed.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
// editor
|
||||
import { TEmbedItem } from "@plane/document-editor";
|
||||
// types
|
||||
import { TPageEmbedResponse } from "@plane/types";
|
||||
// ui
|
||||
import { PriorityIcon } from "@plane/ui";
|
||||
// services
|
||||
import { PageService } from "@/services/page.service";
|
||||
|
||||
const pageService = new PageService();
|
||||
|
||||
export const useIssueEmbed = (workspaceSlug: string, projectId: string) => {
|
||||
const fetchIssues = async (searchQuery: string): Promise<TEmbedItem[]> =>
|
||||
await pageService
|
||||
.searchEmbed<TPageEmbedResponse[]>(workspaceSlug, projectId, {
|
||||
query_type: "issue",
|
||||
query: searchQuery,
|
||||
count: 10,
|
||||
})
|
||||
.then((res) => {
|
||||
const structuredIssues: TEmbedItem[] = (res ?? []).map((issue) => ({
|
||||
id: issue.id,
|
||||
subTitle: `${issue.project__identifier}-${issue.sequence_id}`,
|
||||
title: issue.name,
|
||||
icon: <PriorityIcon priority={issue.priority} />,
|
||||
}));
|
||||
|
||||
return structuredIssues;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw Error(err);
|
||||
});
|
||||
|
||||
return {
|
||||
fetchIssues,
|
||||
};
|
||||
};
|
||||
@@ -3,13 +3,12 @@ import { observer } from "mobx-react";
|
||||
// components
|
||||
import { PageHead } from "@/components/core";
|
||||
import { WorkspaceActiveCycleHeader } from "@/components/headers";
|
||||
import { WorkspaceActiveCyclesUpgrade } from "@/components/workspace";
|
||||
import { WorkspaceActiveCyclesList } from "@/components/workspace";
|
||||
// layouts
|
||||
import { useWorkspace } from "@/hooks/store";
|
||||
import { AppLayout } from "@/layouts/app-layout";
|
||||
// types
|
||||
import { NextPageWithLayout } from "@/lib/types";
|
||||
// hooks
|
||||
|
||||
const WorkspaceActiveCyclesPage: NextPageWithLayout = observer(() => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
@@ -19,7 +18,7 @@ const WorkspaceActiveCyclesPage: NextPageWithLayout = observer(() => {
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<WorkspaceActiveCyclesUpgrade />
|
||||
<WorkspaceActiveCyclesList />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { ReactElement } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
import { IProject } from "@plane/types";
|
||||
// hooks
|
||||
import { PageHead } from "@/components/core";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import { ProjectSettingHeader } from "@/components/headers";
|
||||
import { IntegrationCard } from "@/components/project";
|
||||
import { IntegrationsSettingsLoader } from "@/components/ui";
|
||||
// layouts
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "@/constants/fetch-keys";
|
||||
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
|
||||
// ui
|
||||
// types
|
||||
// fetch-keys
|
||||
// constants
|
||||
|
||||
// services
|
||||
const integrationService = new IntegrationService();
|
||||
const projectService = new ProjectService();
|
||||
|
||||
const ProjectIntegrationsPage: NextPageWithLayout = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// fetch project details
|
||||
const { data: projectDetails } = useSWR<IProject>(
|
||||
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
||||
workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null
|
||||
);
|
||||
// fetch Integrations list
|
||||
const { data: workspaceIntegrations } = useSWR(
|
||||
workspaceSlug ? WORKSPACE_INTEGRATIONS(workspaceSlug as string) : null,
|
||||
() => (workspaceSlug ? integrationService.getWorkspaceIntegrationsList(workspaceSlug as string) : null)
|
||||
);
|
||||
// derived values
|
||||
const isAdmin = projectDetails?.member_role === 20;
|
||||
const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Integrations` : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className={`w-full gap-10 overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
|
||||
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
||||
<h3 className="text-xl font-medium">Integrations</h3>
|
||||
</div>
|
||||
{workspaceIntegrations ? (
|
||||
workspaceIntegrations.length > 0 ? (
|
||||
<div>
|
||||
{workspaceIntegrations.map((integration) => (
|
||||
<IntegrationCard key={integration.integration_detail.id} integration={integration} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full w-full py-8">
|
||||
<EmptyState
|
||||
type={EmptyStateType.PROJECT_SETTINGS_INTEGRATIONS}
|
||||
primaryButtonLink={`/${workspaceSlug}/settings/integrations`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<IntegrationsSettingsLoader />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
ProjectIntegrationsPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<AppLayout withProjectWrapper header={<ProjectSettingHeader title="Integrations Settings" />}>
|
||||
<ProjectSettingLayout>{page}</ProjectSettingLayout>
|
||||
</AppLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectIntegrationsPage;
|
||||
@@ -1,16 +1,16 @@
|
||||
import { observer } from "mobx-react";
|
||||
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";
|
||||
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 { AppLayout } from "layouts/app-layout";
|
||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||
import { AppLayout } from "@/layouts/app-layout";
|
||||
import { WorkspaceSettingLayout } from "@/layouts/settings-layout";
|
||||
// components
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
import { NextPageWithLayout } from "@/lib/types";
|
||||
// constants
|
||||
|
||||
const ImportsPage: NextPageWithLayout = observer(() => {
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
import { ReactElement } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
// hooks
|
||||
// services
|
||||
// layouts
|
||||
// components
|
||||
import { PageHead } from "components/core";
|
||||
import { WorkspaceSettingHeader } from "components/headers";
|
||||
import { SingleIntegrationCard } from "components/integration";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { WorkspaceSettingHeader } from "@/components/headers";
|
||||
import { SingleIntegrationCard } from "@/components/integration";
|
||||
// ui
|
||||
import { IntegrationAndImportExportBanner, IntegrationsSettingsLoader } from "components/ui";
|
||||
// types
|
||||
// fetch-keys
|
||||
import { APP_INTEGRATIONS } from "constants/fetch-keys";
|
||||
import { IntegrationAndImportExportBanner, IntegrationsSettingsLoader } from "@/components/ui";
|
||||
// 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";
|
||||
import { APP_INTEGRATIONS } from "@/constants/fetch-keys";
|
||||
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
||||
// hooks
|
||||
import { useUser, useWorkspace } from "@/hooks/store";
|
||||
// layouts
|
||||
import { AppLayout } from "@/layouts/app-layout";
|
||||
import { WorkspaceSettingLayout } from "@/layouts/settings-layout";
|
||||
// types
|
||||
import { NextPageWithLayout } from "@/lib/types";
|
||||
// services
|
||||
import { IntegrationService } from "@/services/integrations";
|
||||
|
||||
const integrationService = new IntegrationService();
|
||||
|
||||
@@ -38,6 +37,10 @@ const WorkspaceIntegrationsPage: NextPageWithLayout = observer(() => {
|
||||
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Integrations` : undefined;
|
||||
|
||||
const { data: appIntegrations } = useSWR(workspaceSlug && isAdmin ? APP_INTEGRATIONS : null, () =>
|
||||
workspaceSlug && isAdmin ? integrationService.getAppIntegrationsList() : null
|
||||
);
|
||||
|
||||
if (!isAdmin)
|
||||
return (
|
||||
<>
|
||||
@@ -48,10 +51,6 @@ const WorkspaceIntegrationsPage: NextPageWithLayout = observer(() => {
|
||||
</>
|
||||
);
|
||||
|
||||
const { data: appIntegrations } = useSWR(workspaceSlug && isAdmin ? APP_INTEGRATIONS : null, () =>
|
||||
workspaceSlug && isAdmin ? integrationService.getAppIntegrationsList() : null
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
@@ -2,7 +2,7 @@
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
import { APIService } from "@/services/api.service";
|
||||
// types
|
||||
import type { CycleDateCheckData, ICycle, TIssue } from "@plane/types";
|
||||
import type { CycleDateCheckData, ICycle, IWorkspaceActiveCyclesResponse, TIssue } from "@plane/types";
|
||||
// helpers
|
||||
|
||||
export class CycleService extends APIService {
|
||||
@@ -10,6 +10,23 @@ export class CycleService extends APIService {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async workspaceActiveCycles(
|
||||
workspaceSlug: string,
|
||||
cursor: string,
|
||||
per_page: number
|
||||
): Promise<IWorkspaceActiveCyclesResponse> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/active-cycles/`, {
|
||||
params: {
|
||||
per_page,
|
||||
cursor,
|
||||
},
|
||||
})
|
||||
.then((res) => res?.data)
|
||||
.catch((err) => {
|
||||
throw err?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getWorkspaceCycles(workspaceSlug: string): Promise<ICycle[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/cycles/`)
|
||||
.then((response) => response?.data)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// types
|
||||
import { TPage } from "@plane/types";
|
||||
import { TPage, TPageEmbedType } from "@plane/types";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
// services
|
||||
@@ -119,4 +119,22 @@ export class PageService extends APIService {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async searchEmbed<T>(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
params: {
|
||||
query_type: TPageEmbedType;
|
||||
count?: number;
|
||||
query: string;
|
||||
}
|
||||
): Promise<T | undefined> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/search/`, {
|
||||
params,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user