Merge pull request #336 from makeplane/develop

release: bug-fixes and ui/ux improvements for 23 Feb 2023
This commit is contained in:
Vamsi Kurama 2023-02-24 00:03:37 +05:30 committed by GitHub
commit 397a3cec4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 2955 additions and 1782 deletions

View File

@ -733,7 +733,7 @@ urlpatterns = [
name="workspace-integrations",
),
path(
"workspaces/<str:slug>/workspace-integrations/<uuid:pk>/",
"workspaces/<str:slug>/workspace-integrations/<uuid:pk>/provider/",
WorkspaceIntegrationViewSet.as_view(
{
"get": "retrieve",

View File

@ -21,7 +21,10 @@ from plane.db.models import (
APIToken,
)
from plane.api.serializers import IntegrationSerializer, WorkspaceIntegrationSerializer
from plane.utils.integrations.github import get_github_metadata
from plane.utils.integrations.github import (
get_github_metadata,
delete_github_installation,
)
class IntegrationViewSet(BaseViewSet):
@ -77,6 +80,14 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
serializer_class = WorkspaceIntegrationSerializer
model = WorkspaceIntegration
def get_queryset(self):
return (
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.select_related("integration")
)
def create(self, request, slug, provider):
try:
installation_id = request.data.get("installation_id", None)
@ -157,3 +168,31 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def destroy(self, request, slug, pk):
try:
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)
except WorkspaceIntegration.DoesNotExist:
return Response(
{"error": "Workspace Integration Does not exists"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)

View File

@ -46,6 +46,14 @@ class GithubRepositorySyncViewSet(BaseViewSet):
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):
try:
name = request.data.get("name", False)
@ -60,6 +68,23 @@ class GithubRepositorySyncViewSet(BaseViewSet):
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()
# Project member delete
ProjectMember.objects.filter(
member=workspace_integration.actor, role=20, project_id=project_id
).delete()
# Create repository
repo = GithubRepository.objects.create(
name=name,
@ -70,11 +95,6 @@ class GithubRepositorySyncViewSet(BaseViewSet):
project_id=project_id,
)
# Get the workspace integration
workspace_integration = WorkspaceIntegration.objects.get(
pk=workspace_integration_id
)
# Create a Label for github
label = Label.objects.filter(
name="GitHub",

View File

@ -60,3 +60,15 @@ def get_github_repos(access_tokens_url, 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 " + token,
"Accept": "application/vnd.github+json",
}
response = requests.delete(url, headers=headers)
return response

View File

@ -1,5 +1,6 @@
NEXT_PUBLIC_API_BASE_URL = "localhost/"
NEXT_PUBLIC_API_BASE_URL = "http://localhost"
NEXT_PUBLIC_GOOGLE_CLIENTID="<-- google client id -->"
NEXT_PUBLIC_GITHUB_APP_NAME="<-- github app name -->"
NEXT_PUBLIC_GITHUB_ID="<-- github client id -->"
NEXT_PUBLIC_SENTRY_DSN="<-- sentry dns -->"
NEXT_PUBLIC_ENABLE_OAUTH=0

View File

@ -15,7 +15,7 @@ const Breadcrumbs = ({ children }: BreadcrumbsProps) => {
<>
<div className="flex items-center">
<div
className="grid h-8 w-8 cursor-pointer place-items-center rounded border border-gray-300 text-center text-sm hover:bg-gray-100"
className="grid h-8 w-8 cursor-pointer place-items-center flex-shrink-0 rounded border border-gray-300 text-center text-sm hover:bg-gray-100"
onClick={() => router.back()}
>
<ArrowLeftIcon className="h-3 w-3" />

View File

@ -72,7 +72,7 @@ export const SingleBoard: React.FC<Props> = ({
return (
<div className={`h-full flex-shrink-0 rounded ${!isCollapsed ? "" : "w-80 border bg-gray-50"}`}>
<div className={`${!isCollapsed ? "" : "flex h-full flex-col space-y-3 overflow-y-auto"}`}>
<div className={`${!isCollapsed ? "" : "flex h-full flex-col space-y-3"}`}>
<BoardHeader
addIssueToState={addIssueToState}
bgColor={bgColor}
@ -86,7 +86,7 @@ export const SingleBoard: React.FC<Props> = ({
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
{(provided, snapshot) => (
<div
className={`relative mt-3 h-full px-3 pb-3 ${
className={`relative mt-3 h-full px-3 pb-3 overflow-y-auto ${
snapshot.isDraggingOver ? "bg-indigo-50 bg-opacity-50" : ""
} ${!isCollapsed ? "hidden" : "block"}`}
ref={provided.innerRef}

View File

@ -226,12 +226,13 @@ export const SingleBoardIssue: React.FC<Props> = ({
</h5>
</a>
</Link>
<div className="flex flex-wrap items-center gap-x-1 gap-y-2 text-xs">
<div className="relative flex flex-wrap items-center gap-x-1 gap-y-2 text-xs">
{properties.priority && selectedGroup !== "priority" && (
<ViewPrioritySelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
selfPositioned
/>
)}
{properties.state && selectedGroup !== "state_detail.name" && (
@ -239,6 +240,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
selfPositioned
/>
)}
{properties.due_date && (
@ -276,6 +278,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
tooltipPosition="left"
selfPositioned
/>
)}
</div>

View File

@ -8,3 +8,4 @@ export * from "./issues-view-filter";
export * from "./issues-view";
export * from "./link-modal";
export * from "./not-authorized-view";
export * from "./multi-level-select";

View File

@ -0,0 +1,150 @@
import React, { useState } from "react";
import { Listbox, Transition } from "@headlessui/react";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/20/solid";
type TSelectOption = {
id: string;
label: string;
value: any;
children?:
| (TSelectOption & {
children?: null;
})[]
| null;
};
type TMultipleSelectProps = {
options: TSelectOption[];
selected: TSelectOption | null;
setSelected: (value: any) => void;
label: string;
direction?: "left" | "right";
};
export const MultiLevelSelect: React.FC<TMultipleSelectProps> = (props) => {
const { options, selected, setSelected, label, direction = "right" } = props;
const [openChildFor, setOpenChildFor] = useState<TSelectOption | null>(null);
return (
<div className="fixed top-16 w-72">
<Listbox
value={selected}
onChange={(value) => {
if (value?.children === null) {
setSelected(value);
setOpenChildFor(null);
} else setOpenChildFor(value);
}}
>
{({ open }) => (
<div className="relative mt-1">
<Listbox.Button
onClick={() => setOpenChildFor(null)}
className="relative w-full cursor-default rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md sm:text-sm"
>
<span className="block truncate">{selected?.label ?? label}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</span>
</Listbox.Button>
<Transition
as={React.Fragment}
show={open}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options
static
className="absolute mt-1 max-h-60 w-full rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
>
{options.map((option) => (
<Listbox.Option
key={option.id}
className={
"relative cursor-default select-none py-2 pl-10 pr-4 hover:bg-gray-100 hover:text-gray-900"
}
onClick={(e: any) => {
if (option.children !== null) {
e.preventDefault();
setOpenChildFor(option);
}
if (option.id === openChildFor?.id) {
e.preventDefault();
setOpenChildFor(null);
}
}}
value={option}
>
{({ selected }) => (
<>
{openChildFor?.id === option.id && (
<div
className={`w-72 h-auto max-h-72 bg-white border border-gray-200 absolute rounded-lg ${
direction === "right"
? "rounded-tl-none shadow-md left-full translate-x-2"
: "rounded-tr-none shadow-md right-full -translate-x-2"
}`}
>
{option.children?.map((child) => (
<Listbox.Option
key={child.id}
className={
"relative cursor-default select-none py-2 pl-10 pr-4 hover:bg-gray-100 hover:text-gray-900"
}
as="div"
value={child}
>
{({ selected }) => (
<>
<span
className={`block truncate ${
selected ? "font-medium" : "font-normal"
}`}
>
{child.label}
</span>
{selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-600">
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
) : null}
</>
)}
</Listbox.Option>
))}
<div
className={`w-0 h-0 absolute border-t-8 border-gray-300 ${
direction === "right"
? "top-0 left-0 -translate-x-2 border-r-8 border-b-8 border-b-transparent border-t-transparent border-l-transparent"
: "top-0 right-0 translate-x-2 border-l-8 border-b-8 border-b-transparent border-t-transparent border-r-transparent"
}`}
/>
</div>
)}
<span
className={`block truncate ${selected ? "font-medium" : "font-normal"}`}
>
{option.label}
</span>
{selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-600">
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
)}
</Listbox>
</div>
);
};

View File

@ -29,7 +29,7 @@ export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, userAuth }
{links.map((link) => (
<div key={link.id} className="relative">
{!isNotAllowed && (
<div className="absolute top-1.5 right-1.5 z-10 flex items-center gap-1">
<div className="absolute top-1.5 right-1.5 z-[1] flex items-center gap-1">
<Link href={link.url}>
<a
className="grid h-7 w-7 place-items-center rounded bg-gray-100 p-1 outline-none"
@ -56,8 +56,8 @@ export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, userAuth }
<h5 className="w-4/5">{link.title}</h5>
<p className="mt-0.5 text-gray-500">
Added {timeAgo(link.created_at)}
<br />
by {link.created_by_detail.email}
{/* <br />
by {link.created_by_detail.email} */}
</p>
</div>
</a>

View File

@ -87,7 +87,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = (props) => {
<div className="rounded-md border bg-white p-3">
<div className="grid grid-cols-9 gap-2 divide-x">
<div className="col-span-3 flex flex-col space-y-3">
<div className="flex items-center justify-between gap-2">
<div className="flex items-start justify-between gap-2">
<Link href={`/${workspaceSlug}/projects/${projectId as string}/cycles/${cycle.id}`}>
<a>
<h2 className="font-medium w-full max-w-[175px] lg:max-w-[225px] xl:max-w-[300px] text-ellipsis overflow-hidden">

View File

@ -59,7 +59,7 @@ const EmojiIconPicker: React.FC<Props> = ({ label, value, onChange }) => {
leaveTo="transform opacity-0 scale-95"
>
<Popover.Panel className="absolute z-10 mt-2 w-80 rounded-md bg-white shadow-lg">
<div className="h-80 w-80 overflow-auto rounded border bg-white p-2 shadow-2xl">
<div className="h-72 w-80 overflow-auto rounded border bg-white p-2 shadow-2xl">
<Tab.Group as="div" className="flex h-full w-full flex-col">
<Tab.List className="flex-0 -mx-2 flex justify-around gap-1 rounded border-b p-1">
{tabOptions.map((tab) => (

View File

@ -75,6 +75,19 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
}, [projectId, projects]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
handleClose();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, []);
const addIssueToCycle = async (issueId: string, cycleId: string) => {
if (!workspaceSlug || !projectId) return;

View File

@ -15,11 +15,13 @@ import {
} from "components/issues/view-select";
// ui
import { AssigneesList } from "components/ui/avatar";
import { CustomMenu } from "components/ui";
import { CustomMenu, Tooltip } from "components/ui";
// types
import { IIssue, Properties } from "types";
// fetch-keys
import { USER_ISSUE } from "constants/fetch-keys";
import { copyTextToClipboard } from "helpers/string.helper";
import useToast from "hooks/use-toast";
type Props = {
issue: IIssue;
@ -36,6 +38,7 @@ export const MyIssuesListItem: React.FC<Props> = ({
}) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { setToastAlert } = useToast();
const partialUpdateIssue = useCallback(
(formData: Partial<IIssue>) => {
@ -64,6 +67,20 @@ export const MyIssuesListItem: React.FC<Props> = ({
[workspaceSlug, projectId, issue]
);
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",
title: "Link Copied!",
message: "Issue link copied to clipboard.",
});
});
};
const isNotAllowed = false;
return (
@ -78,13 +95,20 @@ export const MyIssuesListItem: React.FC<Props> = ({
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
<a className="group relative flex items-center gap-2">
{properties?.key && (
<span className="flex-shrink-0 text-xs text-gray-500">
{issue.project_detail?.identifier}-{issue.sequence_id}
</span>
<Tooltip
tooltipHeading="ID"
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
>
<span className="flex-shrink-0 text-xs text-gray-500">
{issue.project_detail?.identifier}-{issue.sequence_id}
</span>
</Tooltip>
)}
<span className="w-[275px] md:w-[450px] lg:w-[600px] text-ellipsis overflow-hidden whitespace-nowrap">
{issue.name}
</span>
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<span className="w-auto max-w-lg text-ellipsis overflow-hidden whitespace-nowrap">
{issue.name}
</span>
</Tooltip>
</a>
</Link>
</div>
@ -134,12 +158,27 @@ export const MyIssuesListItem: React.FC<Props> = ({
</div>
)}
{properties.assignee && (
<div className="flex items-center gap-1">
<AssigneesList userIds={issue.assignees ?? []} />
</div>
<Tooltip
position="top-right"
tooltipHeading="Assignees"
tooltipContent={
issue.assignee_details.length > 0
? issue.assignee_details
.map((assignee) =>
assignee?.first_name !== "" ? assignee?.first_name : assignee?.email
)
.join(", ")
: "No Assignee"
}
>
<div className="flex items-center gap-1">
<AssigneesList userIds={issue.assignees ?? []} />
</div>
</Tooltip>
)}
<CustomMenu width="auto" ellipsis>
<CustomMenu.MenuItem onClick={handleDeleteIssue}>Delete permanently</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleDeleteIssue}>Delete issue</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleCopyText}>Copy issue link</CustomMenu.MenuItem>
</CustomMenu>
</div>
</div>

View File

@ -8,7 +8,7 @@ import useSWR, { mutate } from "swr";
import issuesService from "services/issues.service";
import cyclesService from "services/cycles.service";
// ui
import { Spinner, CustomSelect } from "components/ui";
import { Spinner, CustomSelect, Tooltip } from "components/ui";
// icons
import { CyclesIcon } from "components/icons";
// types
@ -65,11 +65,19 @@ export const SidebarCycleSelect: React.FC<Props> = ({
<div className="space-y-1 sm:basis-1/2">
<CustomSelect
label={
<span
className={`hidden truncate text-left sm:block ${issueCycle ? "" : "text-gray-900"}`}
<Tooltip
position="top-right"
tooltipHeading="Cycle"
tooltipContent={issueCycle ? issueCycle.cycle_detail.name : "None"}
>
{issueCycle ? issueCycle.cycle_detail.name : "None"}
</span>
<span
className={` w-full max-w-[125px] truncate text-left sm:block ${
issueCycle ? "" : "text-gray-900"
}`}
>
{issueCycle ? issueCycle.cycle_detail.name : "None"}
</span>
</Tooltip>
}
value={issueCycle?.cycle_detail.id}
onChange={(value: any) => {
@ -84,11 +92,15 @@ export const SidebarCycleSelect: React.FC<Props> = ({
<>
{cycles.map((option) => (
<CustomSelect.Option key={option.id} value={option.id}>
{option.name}
<Tooltip position="left-bottom" tooltipContent={option.name}>
<span className="w-full max-w-[125px] truncate ">{option.name}</span>
</Tooltip>
</CustomSelect.Option>
))}
<CustomSelect.Option value={null} className="capitalize">
None
<Tooltip position="left-bottom" tooltipContent="None">
<span className="w-full max-w-[125px] truncate">None</span>
</Tooltip>
</CustomSelect.Option>
</>
) : (

View File

@ -7,7 +7,7 @@ import useSWR, { mutate } from "swr";
// services
import modulesService from "services/modules.service";
// ui
import { Spinner, CustomSelect } from "components/ui";
import { Spinner, CustomSelect, Tooltip } from "components/ui";
// icons
import { RectangleGroupIcon } from "@heroicons/react/24/outline";
// types
@ -64,11 +64,19 @@ export const SidebarModuleSelect: React.FC<Props> = ({
<div className="space-y-1 sm:basis-1/2">
<CustomSelect
label={
<span
className={`hidden truncate text-left sm:block ${issueModule ? "" : "text-gray-900"}`}
<Tooltip
position="top-right"
tooltipHeading="Module"
tooltipContent={modules?.find((m) => m.id === issueModule?.module)?.name ?? "None"}
>
{modules?.find((m) => m.id === issueModule?.module)?.name ?? "None"}
</span>
<span
className={`w-full max-w-[125px] truncate text-left sm:block ${
issueModule ? "" : "text-gray-900"
}`}
>
{modules?.find((m) => m.id === issueModule?.module)?.name ?? "None"}
</span>
</Tooltip>
}
value={issueModule?.module_detail?.id}
onChange={(value: any) => {
@ -83,11 +91,15 @@ export const SidebarModuleSelect: React.FC<Props> = ({
<>
{modules.map((option) => (
<CustomSelect.Option key={option.id} value={option.id}>
{option.name}
<Tooltip position="left-bottom" tooltipContent={option.name}>
<span className="w-full max-w-[125px] truncate">{option.name}</span>
</Tooltip>
</CustomSelect.Option>
))}
<CustomSelect.Option value={null} className="capitalize">
None
<Tooltip position="left-bottom" tooltipContent="None">
<span className="w-full max-w-[125px] truncate"> None </span>
</Tooltip>
</CustomSelect.Option>
</>
) : (

View File

@ -18,14 +18,16 @@ import { PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = {
issue: IIssue;
partialUpdateIssue: (formData: Partial<IIssue>) => void;
position?: "left" | "right";
selfPositioned?: boolean;
tooltipPosition?: "left" | "right";
isNotAllowed: boolean;
};
export const ViewAssigneeSelect: React.FC<Props> = ({
issue,
partialUpdateIssue,
position = "right",
selfPositioned = false,
tooltipPosition = "right",
isNotAllowed,
}) => {
const router = useRouter();
@ -50,13 +52,14 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
partialUpdateIssue({ assignees_list: newData });
}}
className="group relative flex-shrink-0"
className={`group ${!selfPositioned ? "relative" : ""} flex-shrink-0`}
disabled={isNotAllowed}
>
{({ open }) => (
<div>
<Listbox.Button>
<Tooltip
position={`top-${tooltipPosition}`}
tooltipHeading="Assignees"
tooltipContent={
issue.assignee_details.length > 0
@ -85,11 +88,7 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options
className={`absolute z-10 mt-1 max-h-48 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none ${
position === "left" ? "left-0" : "right-0"
}`}
>
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-48 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg min-w-full ring-1 ring-black ring-opacity-5 focus:outline-none">
{members?.map((member) => (
<Listbox.Option
key={member.member.id}

View File

@ -12,14 +12,14 @@ import { PRIORITIES } from "constants/project";
type Props = {
issue: IIssue;
partialUpdateIssue: (formData: Partial<IIssue>) => void;
position?: "left" | "right";
selfPositioned?: boolean;
isNotAllowed: boolean;
};
export const ViewPrioritySelect: React.FC<Props> = ({
issue,
partialUpdateIssue,
position = "right",
selfPositioned = false,
isNotAllowed,
}) => (
<CustomSelect
@ -53,6 +53,7 @@ export const ViewPrioritySelect: React.FC<Props> = ({
} border-none`}
noChevron
disabled={isNotAllowed}
selfPositioned={selfPositioned}
>
{PRIORITIES?.map((priority) => (
<CustomSelect.Option key={priority} value={priority} className="capitalize">

View File

@ -17,14 +17,14 @@ import { STATE_LIST } from "constants/fetch-keys";
type Props = {
issue: IIssue;
partialUpdateIssue: (formData: Partial<IIssue>) => void;
position?: "left" | "right";
selfPositioned?: boolean;
isNotAllowed: boolean;
};
export const ViewStateSelect: React.FC<Props> = ({
issue,
partialUpdateIssue,
position = "right",
selfPositioned = false,
isNotAllowed,
}) => {
const router = useRouter();
@ -67,6 +67,7 @@ export const ViewStateSelect: React.FC<Props> = ({
maxHeight="md"
noChevron
disabled={isNotAllowed}
selfPositioned={selfPositioned}
>
{states?.map((state) => (
<CustomSelect.Option key={state.id} value={state.id}>

View File

@ -2,12 +2,14 @@ import React, { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import Image from "next/image";
// components
import { DeleteModuleModal } from "components/modules";
// ui
import { AssigneesList, Avatar, CustomMenu } from "components/ui";
// icons
import User from "public/user.png";
import { CalendarDaysIcon } from "@heroicons/react/24/outline";
// helpers
import { renderShortNumericDateFormat } from "helpers/date-time.helper";
@ -67,24 +69,52 @@ export const SingleModuleCard: React.FC<Props> = ({ module, handleEditModule })
</CustomMenu>
</div>
<Link href={`/${workspaceSlug}/projects/${module.project}/modules/${module.id}`}>
<a className="flex flex-col justify-between h-full cursor-pointer rounded-md border bg-white p-3 ">
<span className="w-3/4 text-ellipsis overflow-hidden">{module.name}</span>
<a className="flex flex-col justify-between h-full cursor-default rounded-md border bg-white p-3 ">
<span className="w-3/4 text-ellipsis cursor-pointer overflow-hidden">
{module.name}
</span>
<div className="mt-4 grid grid-cols-2 gap-2 text-xs md:grid-cols-4">
<div className="space-y-2">
<div className="space-y-2 ">
<h6 className="text-gray-500">LEAD</h6>
<div>
<Avatar user={module.lead_detail} />
{module.lead_detail ? (
<Avatar user={module.lead_detail} />
) : (
<div className="flex items-center gap-1">
<Image
src={User}
height="16px"
width="16px"
className="rounded-full"
alt="N/A"
/>
<span>N/A</span>
</div>
)}
</div>
</div>
<div className="space-y-2">
<h6 className="text-gray-500">MEMBERS</h6>
<div className="flex items-center gap-1 text-xs">
<AssigneesList users={module.members_detail} />
<div className="flex items-center gap-1 text-xs">
{module.members_detail && module.members_detail.length > 0 ? (
<AssigneesList users={module.members_detail} length={3} />
) : (
<div className="flex items-center gap-1">
<Image
src={User}
height="16px"
width="16px"
className="rounded-full"
alt="N/A"
/>
<span>N/A</span>
</div>
)}
</div>
</div>
<div className="space-y-2">
<h6 className="text-gray-500">END DATE</h6>
<div className="flex w-min cursor-pointer items-center gap-1 whitespace-nowrap rounded border px-1.5 py-0.5 text-xs shadow-sm">
<div className="flex w-min items-center gap-1 whitespace-nowrap rounded border px-1.5 py-0.5 text-xs shadow-sm">
<CalendarDaysIcon className="h-3 w-3" />
{module.target_date ? renderShortNumericDateFormat(module?.target_date) : "N/A"}
</div>

View File

@ -1,10 +1,29 @@
import { useRouter } from "next/router";
import React, { useRef } from "react";
import React, { useRef, useState } from "react";
import Image from "next/image";
import { useRouter } from "next/router";
// services
import workspaceService from "services/workspace.service";
// hooks
import useToast from "hooks/use-toast";
// ui
import { Button, Loader } from "components/ui";
// icons
import GithubLogo from "public/logos/github-black.png";
import useSWR, { mutate } from "swr";
import { APP_INTEGRATIONS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys";
import { IWorkspaceIntegrations } from "types";
const OAuthPopUp = ({ integration }: any) => {
const [deletingIntegration, setDeletingIntegration] = useState(false);
const OAuthPopUp = ({ workspaceSlug, integration }: any) => {
const popup = useRef<any>();
const router = useRouter();
const { workspaceSlug } = router.query;
const { setToastAlert } = useToast();
const checkPopup = () => {
const check = setInterval(() => {
@ -19,7 +38,9 @@ const OAuthPopUp = ({ workspaceSlug, integration }: any) => {
height = 600;
const left = window.innerWidth / 2 - width / 2;
const top = window.innerHeight / 2 - height / 2;
const url = `https://github.com/apps/${process.env.NEXT_PUBLIC_GITHUB_APP_NAME}/installations/new?state=${workspaceSlug}`;
const url = `https://github.com/apps/${
process.env.NEXT_PUBLIC_GITHUB_APP_NAME
}/installations/new?state=${workspaceSlug as string}`;
return window.open(url, "", `width=${width}, height=${height}, top=${top}, left=${left}`);
};
@ -29,12 +50,106 @@ const OAuthPopUp = ({ workspaceSlug, integration }: any) => {
checkPopup();
};
const { data: workspaceIntegrations } = useSWR(
workspaceSlug ? WORKSPACE_INTEGRATIONS(workspaceSlug as string) : null,
() =>
workspaceSlug ? workspaceService.getWorkspaceIntegrations(workspaceSlug as string) : null
);
const handleRemoveIntegration = async () => {
if (!workspaceSlug || !integration || !workspaceIntegrations) return;
const workspaceIntegrationId = workspaceIntegrations?.find(
(i) => i.integration === integration.id
)?.id;
setDeletingIntegration(true);
await workspaceService
.deleteWorkspaceIntegration(workspaceSlug as string, workspaceIntegrationId ?? "")
.then(() => {
mutate<IWorkspaceIntegrations[]>(
WORKSPACE_INTEGRATIONS(workspaceSlug as string),
(prevData) => prevData?.filter((i) => i.id !== workspaceIntegrationId),
false
);
setDeletingIntegration(false);
setToastAlert({
type: "success",
title: "Deleted successfully!",
message: `${integration.title} integration deleted successfully.`,
});
})
.catch(() => {
setDeletingIntegration(false);
setToastAlert({
type: "error",
title: "Error!",
message: `${integration.title} integration could not be deleted. Please try again.`,
});
});
};
const isInstalled = workspaceIntegrations?.find(
(i: any) => i.integration_detail.id === integration.id
);
return (
<>
<div>
<button onClick={startAuth}>{integration.title}</button>
<div className="flex items-center justify-between gap-2 border p-4 rounded-lg">
<div className="flex items-start gap-4">
<div className="h-12 w-12">
<Image src={GithubLogo} alt="GithubLogo" />
</div>
<div>
<h3 className="flex items-center gap-4 font-semibold text-xl">
{integration.title}
{workspaceIntegrations ? (
isInstalled ? (
<span className="flex items-center text-green-500 font-normal text-sm gap-1">
<span className="h-1.5 w-1.5 bg-green-500 flex-shrink-0 rounded-full" /> Installed
</span>
) : (
<span className="flex items-center text-gray-400 font-normal text-sm gap-1">
<span className="h-1.5 w-1.5 bg-gray-400 flex-shrink-0 rounded-full" /> Not
Installed
</span>
)
) : null}
</h3>
<p className="text-gray-400 text-sm">
{workspaceIntegrations
? isInstalled
? "Activate GitHub integrations on individual projects to sync with specific repositories."
: "Connect with GitHub with your Plane workspace to sync project issues."
: "Loading..."}
</p>
</div>
</div>
</>
{workspaceIntegrations ? (
isInstalled ? (
<Button
theme="danger"
size="rg"
className="text-xs"
onClick={handleRemoveIntegration}
disabled={deletingIntegration}
>
{deletingIntegration ? "Removing..." : "Remove installation"}
</Button>
) : (
<Button theme="secondary" size="rg" className="text-xs" onClick={startAuth}>
Add installation
</Button>
)
) : (
<Loader>
<Loader.Item height="35px" width="150px" />
</Loader>
)}
</div>
);
};

View File

@ -53,10 +53,9 @@ export const ProjectCard: React.FC<ProjectCardProps> = (props) => {
{project.icon && (
<span className="text-base">{String.fromCodePoint(parseInt(project.icon))}</span>
)}
<span className=" max-w-[225px] w-[125px] xl:max-w-[225px] text-ellipsis overflow-hidden">
<span className=" w-auto max-w-[220px] text-ellipsis whitespace-nowrap overflow-hidden">
{project.name}
</span>
<span className="text-xs text-gray-500 ">{project.identifier}</span>
</a>
</Link>
</div>

View File

@ -122,12 +122,12 @@ const ConfirmProjectDeletion: React.FC<TConfirmProjectDeletionProps> = (props) =
aria-hidden="true"
/>
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
Delete Project
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
<p className="text-sm text-gray-500 break-all">
Are you sure you want to delete project - {`"`}
<span className="italic">{selectedProject?.name}</span>
{`"`} ? All of the data related to the project will be permanently
@ -136,7 +136,7 @@ const ConfirmProjectDeletion: React.FC<TConfirmProjectDeletionProps> = (props) =
</div>
<div className="my-3 h-0.5 bg-gray-200" />
<div className="mt-3">
<p className="text-sm">
<p className="text-sm break-all">
Enter the project name{" "}
<span className="font-semibold">{selectedProject?.name}</span> to
continue:

View File

@ -1,4 +1,5 @@
export * from "./create-project-modal";
export * from "./sidebar-list";
export * from "./join-project";
export * from "./card";
export * from "./create-project-modal";
export * from "./join-project";
export * from "./sidebar-list";
export * from "./single-integration-card";

View File

@ -0,0 +1,140 @@
import Image from "next/image";
import useSWR, { mutate } from "swr";
// services
import projectService from "services/project.service";
// hooks
import { useRouter } from "next/router";
import useToast from "hooks/use-toast";
// ui
import { CustomSelect } from "components/ui";
// icons
import GithubLogo from "public/logos/github-black.png";
// types
import { IWorkspaceIntegrations } from "types";
// fetch-keys
import { PROJECT_GITHUB_REPOSITORY } from "constants/fetch-keys";
type Props = {
integration: IWorkspaceIntegrations;
};
export const SingleIntegration: React.FC<Props> = ({ integration }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { setToastAlert } = useToast();
const { data: syncedGithubRepository } = useSWR(
projectId ? PROJECT_GITHUB_REPOSITORY(projectId as string) : null,
() =>
workspaceSlug && projectId && integration
? projectService.getProjectGithubRepository(
workspaceSlug as string,
projectId as string,
integration.id
)
: null
);
const { data: userRepositories } = useSWR("USER_REPOSITORIES", () =>
workspaceSlug && integration
? projectService.getGithubRepositories(workspaceSlug as any, integration.id)
: null
);
const handleChange = (repo: any) => {
if (!workspaceSlug || !projectId || !integration) return;
const {
html_url,
owner: { login },
id,
name,
} = repo;
projectService
.syncGiuthubRepository(workspaceSlug as string, projectId as string, integration.id, {
name,
owner: login,
repository_id: id,
url: html_url,
})
.then((res) => {
console.log(res);
mutate(PROJECT_GITHUB_REPOSITORY(projectId as string));
setToastAlert({
type: "success",
title: "Success!",
message: `${login}/${name} respository synced with the project successfully.`,
});
})
.catch((err) => {
console.log(err);
setToastAlert({
type: "error",
title: "Error!",
message: "Respository could not be synced with the project. Please try again.",
});
});
};
return (
<>
{integration && (
<div className="flex items-center justify-between gap-2 border p-4 rounded-xl">
<div className="flex items-start gap-4">
<div className="h-12 w-12">
<Image src={GithubLogo} alt="GithubLogo" />
</div>
<div>
<h3 className="flex items-center gap-4 font-semibold text-xl">
{integration.integration_detail.title}
</h3>
<p className="text-gray-400 text-sm">Select GitHub repository to enable sync.</p>
</div>
</div>
<CustomSelect
value={
syncedGithubRepository && syncedGithubRepository.length > 0
? `${syncedGithubRepository[0].repo_detail.owner}/${syncedGithubRepository[0].repo_detail.name}`
: null
}
onChange={(val: string) => {
const repo = userRepositories?.repositories.find((repo) => repo.full_name === val);
handleChange(repo);
}}
label={
syncedGithubRepository && syncedGithubRepository.length > 0
? `${syncedGithubRepository[0].repo_detail.owner}/${syncedGithubRepository[0].repo_detail.name}`
: "Select Repository"
}
input
>
{userRepositories ? (
userRepositories.repositories.length > 0 ? (
userRepositories.repositories.map((repo) => (
<CustomSelect.Option
key={repo.id}
value={repo.full_name}
className="flex items-center gap-2"
>
<>{repo.full_name}</>
</CustomSelect.Option>
))
) : (
<p className="text-gray-400 text-center text-xs">No repositories found</p>
)
) : (
<p className="text-gray-400 text-center text-xs">Loading repositories</p>
)}
</CustomSelect>
</div>
)}
</>
);
};

View File

@ -143,7 +143,7 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
}),
new TableExtension(),
],
content: value,
content: !value || (typeof value === "object" && Object.keys(value).length === 0) ? "" : value,
selection: "start",
stringHandler: "html",
onError,
@ -153,7 +153,12 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
(value: any) => {
// Clear out old state when setting data from outside
// This prevents e.g. the user from using CTRL-Z to go back to the old state
manager.view.updateState(manager.createState({ content: value ? value : "" }));
manager.view.updateState(
manager.createState({
content:
!value || (typeof value === "object" && Object.keys(value).length === 0) ? "" : value,
})
);
},
[manager]
);

View File

@ -172,7 +172,11 @@ export const FloatingLinkToolbar = () => {
return (
<>
{!isEditing && <FloatingToolbar>{linkEditButtons}</FloatingToolbar>}
{!isEditing && (
<FloatingToolbar className="shadow-lg rounded bg-white p-1">
{linkEditButtons}
</FloatingToolbar>
)}
{!isEditing && empty && (
<FloatingToolbar positioner={linkPositioner} className="shadow-lg rounded bg-white p-1">
{linkEditButtons}

View File

@ -15,7 +15,9 @@ type CustomSelectProps = {
input?: boolean;
noChevron?: boolean;
buttonClassName?: string;
optionsClassName?: string;
disabled?: boolean;
selfPositioned?: boolean;
};
const CustomSelect = ({
@ -29,13 +31,15 @@ const CustomSelect = ({
input = false,
noChevron = false,
buttonClassName = "",
optionsClassName = "",
disabled = false,
selfPositioned = false,
}: CustomSelectProps) => (
<Listbox
as="div"
value={value}
onChange={onChange}
className="relative flex-shrink-0 text-left"
className={`${!selfPositioned ? "relative" : ""} flex-shrink-0 text-left`}
disabled={disabled}
>
<div>
@ -67,8 +71,8 @@ const CustomSelect = ({
leaveTo="transform opacity-0 scale-95"
>
<Listbox.Options
className={`absolute right-0 z-10 mt-1 origin-top-right overflow-y-auto rounded-md bg-white text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none ${
width === "auto" ? "min-w-full whitespace-nowrap" : "w-56"
className={`${optionsClassName} absolute right-0 z-10 mt-1 origin-top-right overflow-y-auto rounded-md bg-white text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none ${
width === "auto" ? "min-w-full whitespace-nowrap" : width
} ${input ? "max-h-48" : ""} ${
maxHeight === "lg"
? "max-h-60"
@ -97,9 +101,9 @@ const Option: React.FC<OptionProps> = ({ children, value, className }) => (
<Listbox.Option
value={value}
className={({ active, selected }) =>
`${active || selected ? "bg-indigo-50" : ""} ${
`${className} ${active || selected ? "bg-indigo-50" : ""} ${
selected ? "font-medium" : ""
} relative flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900 ${className}`
} relative flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900`
}
>
{children}

View File

@ -5,9 +5,25 @@ import { Tooltip2 } from "@blueprintjs/popover2";
type Props = {
tooltipHeading?: string;
tooltipContent: string;
position?: "top" | "right" | "bottom" | "left";
position?:
| "top"
| "right"
| "bottom"
| "left"
| "auto"
| "auto-end"
| "auto-start"
| "bottom-left"
| "bottom-right"
| "left-bottom"
| "left-top"
| "right-bottom"
| "right-top"
| "top-left"
| "top-right";
children: JSX.Element;
disabled?: boolean;
className?: string;
};
export const Tooltip: React.FC<Props> = ({
@ -16,11 +32,14 @@ export const Tooltip: React.FC<Props> = ({
position = "top",
children,
disabled = false,
className = "",
}) => (
<Tooltip2
disabled={disabled}
content={
<div className="flex flex-col justify-center items-start gap-1 max-w-[600px] text-xs rounded-md bg-white p-2 shadow-md capitalize text-left">
<div
className={`flex flex-col justify-center items-start gap-1 max-w-[600px] text-xs rounded-md bg-white p-2 shadow-md capitalize text-left ${className}`}
>
{tooltipHeading ? (
<>
<h5 className="font-medium">{tooltipHeading}</h5>

View File

@ -29,6 +29,8 @@ export const PROJECT_ISSUES_COMMENTS = (issueId: string) => `PROJECT_ISSUES_COMM
export const PROJECT_ISSUES_ACTIVITY = (issueId: string) => `PROJECT_ISSUES_ACTIVITY_${issueId}`;
export const PROJECT_ISSUE_BY_STATE = (projectId: string) => `PROJECT_ISSUE_BY_STATE_${projectId}`;
export const PROJECT_ISSUE_LABELS = (projectId: string) => `PROJECT_ISSUE_LABELS_${projectId}`;
export const PROJECT_GITHUB_REPOSITORY = (projectId: string) =>
`PROJECT_GITHUB_REPOSITORY_${projectId}`;
export const CYCLE_LIST = (projectId: string) => `CYCLE_LIST_${projectId}`;
export const CYCLE_ISSUES = (cycleId: string) => `CYCLE_ISSUES_${cycleId}`;

View File

@ -229,7 +229,10 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
});
if (!workspaceSlug || !projectId) return;
saveDataToServer(workspaceSlug as string, projectId as string, state);
saveDataToServer(workspaceSlug as string, projectId as string, {
...state,
orderBy: property,
});
},
[projectId, workspaceSlug, state]
);

View File

@ -4,6 +4,9 @@ export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, "
export const capitalizeFirstLetter = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
export const truncateText = (str: string, length: number) =>
str.length > length ? `${str.substring(0, length)}...` : str;
export const createSimilarString = (str: string) => {
const shuffled = str
.split("")

View File

@ -14,7 +14,7 @@ import useIssues from "hooks/use-issues";
// components
import { WorkspaceHomeCardsList, WorkspaceHomeGreetings } from "components/workspace";
// ui
import { Spinner } from "components/ui";
import { Spinner, Tooltip } from "components/ui";
// icons
import {
ArrowRightIcon,
@ -90,69 +90,78 @@ const WorkspacePage: NextPage = () => {
href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}
>
<a className="group relative flex items-center gap-2">
<span className="">{issue.name}</span>
<Tooltip
position="top-left"
tooltipHeading="Title"
tooltipContent={issue.name}
>
<span className="w-auto max-w-[225px] md:max-w-[175px] lg:max-w-xs xl:max-w-sm text-ellipsis overflow-hidden whitespace-nowrap">
{issue.name}
</span>
</Tooltip>
</a>
</Link>
</div>
<div className="flex flex-shrink-0 flex-wrap items-center gap-x-1 gap-y-2 text-xs">
<div
className={`cursor-pointer rounded px-2 py-1 capitalize shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
issue.priority === "urgent"
? "bg-red-100 text-red-600"
: issue.priority === "high"
? "bg-orange-100 text-orange-500"
: issue.priority === "medium"
? "bg-yellow-100 text-yellow-500"
: issue.priority === "low"
? "bg-green-100 text-green-500"
: "bg-gray-100"
}`}
<Tooltip
tooltipHeading="Priority"
tooltipContent={issue.priority ?? "None"}
>
{getPriorityIcon(issue.priority)}
</div>
<div className="flex cursor-pointer items-center gap-1 rounded border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
<span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: issue.state_detail.color,
}}
/>
{addSpaceIfCamelCase(issue.state_detail.name)}
</div>
<div
className={`group group relative flex flex-shrink-0 cursor-pointer items-center gap-1 rounded border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
issue.target_date === null
? ""
: issue.target_date < new Date().toISOString()
? "text-red-600"
: findHowManyDaysLeft(issue.target_date) <= 3 &&
"text-orange-400"
}`}
>
<CalendarDaysIcon className="h-4 w-4" />
{issue.target_date
? renderShortNumericDateFormat(issue.target_date)
: "N/A"}
<div className="absolute bottom-full right-0 z-10 mb-2 hidden whitespace-nowrap rounded-md bg-white p-2 shadow-md group-hover:block">
<h5 className="mb-1 font-medium text-gray-900">Target date</h5>
<div>
{renderShortNumericDateFormat(issue.target_date ?? "")}
</div>
<div>
{issue.target_date &&
(issue.target_date < new Date().toISOString()
? `Target date has passed by ${findHowManyDaysLeft(
issue.target_date
)} days`
: findHowManyDaysLeft(issue.target_date) <= 3
? `Target date is in ${findHowManyDaysLeft(
issue.target_date
)} days`
: "Target date")}
</div>
<div
className={`cursor-pointer rounded px-2 py-1 capitalize shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
issue.priority === "urgent"
? "bg-red-100 text-red-600"
: issue.priority === "high"
? "bg-orange-100 text-orange-500"
: issue.priority === "medium"
? "bg-yellow-100 text-yellow-500"
: issue.priority === "low"
? "bg-green-100 text-green-500"
: "bg-gray-100"
}`}
>
{getPriorityIcon(issue.priority)}
</div>
</div>
</Tooltip>
<Tooltip
tooltipHeading="State"
tooltipContent={addSpaceIfCamelCase(issue.state_detail.name)}
>
<div className="flex cursor-pointer items-center gap-1 rounded border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
<span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: issue.state_detail.color,
}}
/>
<span>{addSpaceIfCamelCase(issue.state_detail.name)}</span>
</div>
</Tooltip>
<Tooltip
tooltipHeading="Due Date"
tooltipContent={
issue.target_date
? renderShortNumericDateFormat(issue.target_date)
: "N/A"
}
>
<div
className={`flex flex-shrink-0 cursor-pointer items-center gap-1 rounded border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
issue.target_date === null
? ""
: issue.target_date < new Date().toISOString()
? "text-red-600"
: findHowManyDaysLeft(issue.target_date) <= 3 &&
"text-orange-400"
}`}
>
<CalendarDaysIcon className="h-4 w-4" />
{issue.target_date
? renderShortNumericDateFormat(issue.target_date)
: "N/A"}
</div>
</Tooltip>
</div>
</div>
))}

View File

@ -23,6 +23,8 @@ import projectService from "services/project.service";
// ui
import { CustomMenu, EmptySpace, EmptySpaceItem, Spinner } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// helpers
import { truncateText } from "helpers/string.helper";
// types
import { CycleIssueResponse, UserAuth } from "types";
// fetch-keys
@ -135,7 +137,7 @@ const SingleCycle: React.FC<UserAuth> = (props) => {
label={
<>
<CyclesIcon className="h-3 w-3" />
{cycleDetails?.name}
{cycleDetails?.name && truncateText(cycleDetails.name, 40)}
</>
}
className="ml-1.5"
@ -147,7 +149,7 @@ const SingleCycle: React.FC<UserAuth> = (props) => {
renderAs="a"
href={`/${workspaceSlug}/projects/${activeProject?.id}/cycles/${cycle.id}`}
>
{cycle.name}
{truncateText(cycle.name, 40)}
</CustomMenu.MenuItem>
))}
</CustomMenu>

View File

@ -27,6 +27,8 @@ import { ModuleDetailsSidebar } from "components/modules";
// ui
import { CustomMenu, EmptySpace, EmptySpaceItem, Spinner } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// helpers
import { truncateText } from "helpers/string.helper";
// types
import { IModule, ModuleIssueResponse, UserAuth } from "types";
@ -130,7 +132,7 @@ const SingleModule: React.FC<UserAuth> = (props) => {
label={
<>
<RectangleGroupIcon className="h-3 w-3" />
{moduleDetails?.name}
{moduleDetails?.name && truncateText(moduleDetails.name, 40)}
</>
}
className="ml-1.5"
@ -142,7 +144,7 @@ const SingleModule: React.FC<UserAuth> = (props) => {
renderAs="a"
href={`/${workspaceSlug}/projects/${projectId}/modules/${module.id}`}
>
{module.name}
{truncateText(module.name, 40)}
</CustomMenu.MenuItem>
))}
</CustomMenu>

View File

@ -20,7 +20,7 @@ import { Button, CustomSelect, Loader } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// types
import { IProject, IWorkspace } from "types";
import type { NextPageContext, NextPage } from "next";
import type { NextPage, GetServerSidePropsContext } from "next";
// fetch-keys
import { PROJECTS_LIST, PROJECT_DETAILS, WORKSPACE_MEMBERS } from "constants/fetch-keys";
@ -251,7 +251,7 @@ const ControlSettings: NextPage<TControlSettingsProps> = (props) => {
);
};
export const getServerSideProps = async (ctx: NextPageContext) => {
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const projectId = ctx.query.projectId as string;
const workspaceSlug = ctx.query.workspaceSlug as string;

View File

@ -17,7 +17,7 @@ import { Button } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// types
import { IProject, UserAuth } from "types";
import type { NextPage, NextPageContext } from "next";
import type { NextPage, GetServerSidePropsContext } from "next";
// fetch-keys
import { PROJECTS_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
@ -168,7 +168,7 @@ const FeaturesSettings: NextPage<UserAuth> = (props) => {
);
};
export const getServerSideProps = async (ctx: NextPageContext) => {
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const projectId = ctx.query.projectId as string;
const workspaceSlug = ctx.query.workspaceSlug as string;

View File

@ -25,7 +25,7 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// helpers
import { debounce } from "helpers/common.helper";
// types
import type { NextPage, NextPageContext } from "next";
import type { NextPage, GetServerSidePropsContext } from "next";
// fetch-keys
import { PROJECTS_LIST, PROJECT_DETAILS, WORKSPACE_DETAILS } from "constants/fetch-keys";
// constants
@ -339,7 +339,7 @@ const GeneralSettings: NextPage<UserAuth> = (props) => {
);
};
export const getServerSideProps = async (ctx: NextPageContext) => {
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const projectId = ctx.query.projectId as string;
const workspaceSlug = ctx.query.workspaceSlug as string;

View File

@ -1,9 +1,8 @@
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import { useRouter } from "next/router";
import Image from "next/image";
import useSWR, { mutate } from "swr";
import useSWR from "swr";
// lib
import { requiredAdmin } from "lib/auth";
@ -12,34 +11,23 @@ import AppLayout from "layouts/app-layout";
// services
import workspaceService from "services/workspace.service";
import projectService from "services/project.service";
// ui
import { EmptySpace, EmptySpaceItem, Loader } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { PlusIcon, PuzzlePieceIcon } from "@heroicons/react/24/outline";
// types
import { IProject, IWorkspace } from "types";
import { IProject, UserAuth } from "types";
import type { NextPageContext, NextPage } from "next";
// fetch-keys
import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys";
import { SingleIntegration } from "components/project";
type TProjectIntegrationsProps = {
isMember: boolean;
isOwner: boolean;
isViewer: boolean;
isGuest: boolean;
};
const defaultValues: Partial<IProject> = {
project_lead: null,
default_assignee: null,
};
const ProjectIntegrations: NextPage<TProjectIntegrationsProps> = (props) => {
const ProjectIntegrations: NextPage<UserAuth> = (props) => {
const { isMember, isOwner, isViewer, isGuest } = props;
const [userRepos, setUserRepos] = useState([]);
const [activeIntegrationId, setActiveIntegrationId] = useState();
const {
query: { workspaceSlug, projectId },
} = useRouter();
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: projectDetails } = useSWR<IProject>(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
@ -48,34 +36,12 @@ const ProjectIntegrations: NextPage<TProjectIntegrationsProps> = (props) => {
: null
);
const { data: integrations } = useSWR(
const { data: workspaceIntegrations } = useSWR(
workspaceSlug ? WORKSPACE_INTEGRATIONS(workspaceSlug as string) : null,
() =>
workspaceSlug ? workspaceService.getWorkspaceIntegrations(workspaceSlug as string) : null
);
const handleChange = (repo: any) => {
const {
html_url,
owner: { login },
id,
name,
} = repo;
projectService
.syncGiuthubRepository(
workspaceSlug as string,
projectId as string,
activeIntegrationId as any,
{ name, owner: login, repository_id: id, url: html_url }
)
.then((res) => {
console.log(res);
})
.catch((err) => {
console.log(err);
});
};
console.log(userRepos);
return (
<AppLayout
settingsLayout="project"
@ -90,41 +56,45 @@ const ProjectIntegrations: NextPage<TProjectIntegrationsProps> = (props) => {
</Breadcrumbs>
}
>
<section className="space-y-8">
{integrations?.map((integration: any) => (
<div
key={integration.id}
onClick={() => {
setActiveIntegrationId(integration.id);
projectService
.getGithubRepositories(workspaceSlug as any, integration.id)
.then((response) => {
setUserRepos(response.repositories);
})
.catch((err) => {
console.log(err);
});
}}
>
{integration.integration_detail.provider}
</div>
))}
{userRepos.length > 0 && (
<select
onChange={(e) => {
const repo = userRepos.find((repo: any) => repo.id == e.target.value);
handleChange(repo);
}}
>
<option value={undefined}>Select Repository</option>
{userRepos?.map((repo: any) => (
<option value={repo.id} key={repo.id}>
{repo.full_name}
</option>
{workspaceIntegrations ? (
workspaceIntegrations.length > 0 ? (
<section className="space-y-8">
<div>
<h3 className="text-3xl font-bold leading-6 text-gray-900">Integrations</h3>
<p className="mt-4 text-sm text-gray-500">Manage the project integrations.</p>
</div>
{workspaceIntegrations.map((integration) => (
<SingleIntegration
key={integration.integration_detail.id}
integration={integration}
/>
))}
</select>
)}
</section>
</section>
) : (
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
<EmptySpace
title="You haven't added any integration yet."
description="Add GitHub and other integrations to sync your project issues."
Icon={PuzzlePieceIcon}
>
<EmptySpaceItem
title="Add new integration"
Icon={PlusIcon}
action={() => {
router.push(`/${workspaceSlug}/settings/integrations`);
}}
/>
</EmptySpace>
</div>
)
) : (
<Loader className="space-y-5 md:w-2/3">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
)}
</AppLayout>
);
};

View File

@ -25,7 +25,7 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
import { PlusIcon } from "@heroicons/react/24/outline";
// types
import { IIssueLabels, UserAuth } from "types";
import type { NextPageContext, NextPage } from "next";
import type { GetServerSidePropsContext, NextPage } from "next";
// fetch-keys
import { PROJECT_DETAILS, PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
@ -173,7 +173,7 @@ const LabelsSettings: NextPage<UserAuth> = (props) => {
);
};
export const getServerSideProps = async (ctx: NextPageContext) => {
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const projectId = ctx.query.projectId as string;
const workspaceSlug = ctx.query.workspaceSlug as string;

View File

@ -23,7 +23,7 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// types
import type { NextPage, NextPageContext } from "next";
import type { NextPage, GetServerSidePropsContext } from "next";
// fetch-keys
import {
PROJECT_DETAILS,
@ -313,7 +313,7 @@ const MembersSettings: NextPage<TMemberSettingsProps> = (props) => {
);
};
export const getServerSideProps = async (ctx: NextPageContext) => {
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const projectId = ctx.query.projectId as string;
const workspaceSlug = ctx.query.workspaceSlug as string;

View File

@ -27,7 +27,7 @@ import { PlusIcon } from "@heroicons/react/24/outline";
import { getStatesList, orderStateGroups } from "helpers/state.helper";
// types
import { UserAuth } from "types";
import type { NextPage, NextPageContext } from "next";
import type { NextPage, GetServerSidePropsContext } from "next";
// fetch-keys
import { PROJECT_DETAILS, STATE_LIST } from "constants/fetch-keys";
@ -155,7 +155,7 @@ const StatesSettings: NextPage<UserAuth> = (props) => {
);
};
export const getServerSideProps = async (ctx: NextPageContext) => {
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const projectId = ctx.query.projectId as string;
const workspaceSlug = ctx.query.workspaceSlug as string;

View File

@ -1,32 +1,29 @@
import React from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// lib
import type { NextPage, GetServerSideProps } from "next";
import { requiredWorkspaceAdmin } from "lib/auth";
// constants
// services
import workspaceService from "services/workspace.service";
// lib
import { requiredWorkspaceAdmin } from "lib/auth";
// layouts
import AppLayout from "layouts/app-layout";
// ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
import { WORKSPACE_DETAILS, APP_INTEGRATIONS } from "constants/fetch-keys";
// componentss
import OAuthPopUp from "components/popup";
// ui
import { Loader } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// types
import type { NextPage, GetServerSideProps } from "next";
import { UserAuth } from "types";
// fetch-keys
import { WORKSPACE_DETAILS, APP_INTEGRATIONS } from "constants/fetch-keys";
type TWorkspaceIntegrationsProps = {
isOwner: boolean;
isMember: boolean;
isViewer: boolean;
isGuest: boolean;
};
const WorkspaceIntegrations: NextPage<TWorkspaceIntegrationsProps> = (props) => {
const {
query: { workspaceSlug },
} = useRouter();
const WorkspaceIntegrations: NextPage<UserAuth> = (props) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { data: activeWorkspace } = useSWR(
workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null,
@ -53,13 +50,26 @@ const WorkspaceIntegrations: NextPage<TWorkspaceIntegrationsProps> = (props) =>
}
>
<section className="space-y-8">
{integrations?.map((integration: any) => (
<OAuthPopUp
workspaceSlug={workspaceSlug}
key={integration.id}
integration={integration}
/>
))}
<div>
<h3 className="text-3xl font-bold leading-6 text-gray-900">Integrations</h3>
<p className="mt-4 text-sm text-gray-500">Manage the workspace integrations.</p>
</div>
<div className="space-y-4">
{integrations ? (
integrations.map((integration) => (
<OAuthPopUp
key={integration.id}
workspaceSlug={workspaceSlug}
integration={integration}
/>
))
) : (
<Loader className="space-y-5">
<Loader.Item height="60px" />
<Loader.Item height="60px" />
</Loader>
)}
</div>
</section>
</AppLayout>
</>

View File

@ -1,5 +1,9 @@
import React, { useEffect } from "react";
import appinstallationsService from "services/appinstallations.service";
// services
import appinstallationsService from "services/app-installations.service";
// components
import { Spinner } from "components/ui";
interface IGithuPostInstallationProps {
installation_id: string;
@ -28,7 +32,13 @@ const AppPostInstallation = ({
});
}
}, [state, installation_id, provider]);
return <>Loading...</>;
return (
<div className="absolute top-0 left-0 z-50 flex h-full w-full flex-col items-center justify-center gap-y-3 bg-white">
<h2 className="text-2xl text-gray-900">Installing. Please wait...</h2>
<Spinner />
</div>
);
};
export async function getServerSideProps(context: any) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,7 +1,13 @@
// services
import APIService from "services/api.service";
// types
import type { IProject, IProjectMember, IProjectMemberInvitation, ProjectViewTheme } from "types";
import type {
GithubRepositoriesResponse,
IProject,
IProjectMember,
IProjectMemberInvitation,
ProjectViewTheme,
} from "types";
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
@ -202,7 +208,10 @@ class ProjectServices extends APIService {
});
}
async getGithubRepositories(slug: string, workspaceIntegrationId: string): Promise<any> {
async getGithubRepositories(
slug: string,
workspaceIntegrationId: string
): Promise<GithubRepositoriesResponse> {
return this.get(
`/api/workspaces/${slug}/workspace-integrations/${workspaceIntegrationId}/github-repositories/`
)
@ -232,6 +241,20 @@ class ProjectServices extends APIService {
throw error?.response?.data;
});
}
async getProjectGithubRepository(
workspaceSlug: string,
projectId: string,
integrationId: string
): Promise<any> {
return this.get(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/workspace-integrations/${integrationId}/github-repository-sync/`
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
}
export default new ProjectServices();

View File

@ -9,6 +9,8 @@ import {
IWorkspaceMember,
IWorkspaceMemberInvitation,
ILastActiveWorkspaceDetails,
IAppIntegrations,
IWorkspaceIntegrations,
} from "types";
class WorkspaceService extends APIService {
@ -169,20 +171,32 @@ class WorkspaceService extends APIService {
throw error?.response?.data;
});
}
async getIntegrations(): Promise<any> {
async getIntegrations(): Promise<IAppIntegrations[]> {
return this.get(`/api/integrations/`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async getWorkspaceIntegrations(slug: string): Promise<any> {
return this.get(`/api/workspaces/${slug}/workspace-integrations/`)
async getWorkspaceIntegrations(workspaceSlug: string): Promise<IWorkspaceIntegrations[]> {
return this.get(`/api/workspaces/${workspaceSlug}/workspace-integrations/`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async deleteWorkspaceIntegration(workspaceSlug: string, integrationId: string): Promise<any> {
return this.delete(
`/api/workspaces/${workspaceSlug}/workspace-integrations/${integrationId}/provider/`
)
.then((res) => res?.data)
.catch((error) => {
throw error?.response?.data;
});
}
}
export default new WorkspaceService();

View File

@ -23,6 +23,10 @@
-webkit-font-smoothing: antialiased;
}
.scrollbar-enable::-webkit-scrollbar {
display: block ;
}
/* Scrollbar style */
::-webkit-scrollbar {
display: none;

View File

@ -61,3 +61,15 @@ export interface IProjectMemberInvitation {
created_by: string;
updated_by: string;
}
export interface IGithubRepository {
id: string;
full_name: string;
html_url: string;
url: string;
}
export interface GithubRepositoriesResponse {
repositories: IGithubRepository[];
total_count: number;
}

View File

@ -44,3 +44,38 @@ export interface ILastActiveWorkspaceDetails {
workspace_details: IWorkspace;
project_details?: IProjectMember[];
}
export interface IAppIntegrations {
author: string;
author: "";
avatar_url: string | null;
created_at: string;
created_by: string | null;
description: any;
id: string;
metadata: any;
network: number;
provider: string;
redirect_url: string;
title: string;
updated_at: string;
updated_by: string | null;
verified: boolean;
webhook_secret: string;
webhook_url: string;
}
export interface IWorkspaceIntegrations {
actor: string;
api_token: string;
config: any;
created_at: string;
created_by: string;
id: string;
integration: string;
integration_detail: IIntegrations;
metadata: anyl;
updated_at: string;
updated_by: string;
workspace: string;
}

3469
yarn.lock

File diff suppressed because it is too large Load Diff