feat: github importer (#722)

* chore: github importer first step completed

* refactor: github importer code refactored

* chore: github importer functionality completed

* fix: import data step saved data
This commit is contained in:
Aaryan Khandelwal 2023-04-06 00:51:15 +05:30 committed by GitHub
parent 6b8b981e1d
commit c9d8a8dbd1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1211 additions and 718 deletions

View File

@ -142,7 +142,9 @@ export const BoardHeader: React.FC<Props> = ({
>
{getGroupTitle()}
</h2>
<span className="ml-0.5 rounded-full bg-gray-100 py-1 px-3 text-sm">
<span
className={`${isCollapsed ? "ml-0.5" : ""} rounded-full bg-gray-100 py-1 px-3 text-sm`}
>
{groupedByIssues?.[groupTitle].length ?? 0}
</span>
</div>

View File

@ -1,5 +1,8 @@
import { FC, useRef, useState } from "react";
// ui
import { PrimaryButton } from "components/ui";
type Props = {
workspaceSlug: string | undefined;
workspaceIntegration: any;
@ -39,21 +42,11 @@ export const GithubAuth: FC<Props> = ({ workspaceSlug, workspaceIntegration }) =
return (
<div>
{workspaceIntegration && workspaceIntegration?.id ? (
<button
type="button"
className={`cursor-not-allowed rounded-sm bg-theme bg-opacity-80 px-3 py-1.5 text-sm text-white transition-colors`}
>
Successfully Connected
</button>
<PrimaryButton disabled>Successfully Connected</PrimaryButton>
) : (
<button
onClick={startAuth}
type="button"
className={`rounded-sm bg-theme px-3 py-1.5 text-sm text-white transition-colors hover:bg-opacity-80`}
disabled={authLoader}
>
<PrimaryButton onClick={startAuth} loading={authLoader}>
{authLoader ? "Connecting..." : "Connect"}
</button>
</PrimaryButton>
)}
</div>
);

View File

@ -1,74 +0,0 @@
import { FC } from "react";
// components
import { IIntegrationData, GithubAuth } from "components/integration";
// types
import { IAppIntegrations } from "types";
type Props = {
state: IIntegrationData;
provider: string | undefined;
handleState: (key: string, valve: any) => void;
workspaceSlug: string | undefined;
allIntegrations: IAppIntegrations[] | undefined;
allIntegrationsError: Error | undefined;
allWorkspaceIntegrations: any | undefined;
allWorkspaceIntegrationsError: Error | undefined;
};
export const GithubConfigure: FC<Props> = ({
state,
handleState,
workspaceSlug,
provider,
allIntegrations,
allIntegrationsError,
allWorkspaceIntegrations,
allWorkspaceIntegrationsError,
}) => {
// current integration from all the integrations available
const integration =
allIntegrations &&
allIntegrations.length > 0 &&
allIntegrations.find((_integration) => _integration.provider === provider);
// current integration from workspace integrations
const workspaceIntegration =
integration &&
allWorkspaceIntegrations &&
allWorkspaceIntegrations.length > 0 &&
allWorkspaceIntegrations.find(
(_integration: any) => _integration.integration_detail.id === integration.id
);
console.log("integration", integration);
console.log("workspaceIntegration", workspaceIntegration);
return (
<div className="space-y-5">
<div className="flex items-center gap-2 py-5">
<div className="w-full">
<div className="font-medium">Configure</div>
<div className="text-sm text-gray-600">Set up your Github import</div>
</div>
<div className="flex-shrink-0">
<GithubAuth workspaceSlug={workspaceSlug} workspaceIntegration={workspaceIntegration} />
</div>
</div>
<div className="flex items-center justify-end">
<button
type="button"
className={`rounded-sm bg-theme px-3 py-1.5 text-sm text-white transition-colors hover:bg-opacity-80 ${
workspaceIntegration && workspaceIntegration?.id
? `bg-opacity-100`
: `cursor-not-allowed bg-opacity-80`
}`}
onClick={() => handleState("state", "import-import-data")}
disabled={workspaceIntegration && workspaceIntegration?.id ? false : true}
>
Next
</button>
</div>
</div>
);
};

View File

@ -1,27 +0,0 @@
import { FC } from "react";
// types
import { IIntegrationData } from "components/integration";
type Props = { state: IIntegrationData; handleState: (key: string, valve: any) => void };
export const GithubConfirm: FC<Props> = ({ state, handleState }) => (
<>
<div>Confirm</div>
<div className="mt-5 flex items-center justify-between">
<button
type="button"
className={`rounded-sm bg-gray-300 px-3 py-1.5 text-sm transition-colors hover:bg-opacity-80`}
onClick={() => handleState("state", "migrate-users")}
>
Back
</button>
<button
type="button"
className={`rounded-sm bg-theme px-3 py-1.5 text-sm text-white transition-colors hover:bg-opacity-80`}
onClick={() => handleState("state", "migrate-confirm")}
>
Next
</button>
</div>
</>
);

View File

@ -0,0 +1,66 @@
import { FC } from "react";
import { useRouter } from "next/router";
// components
import { GithubAuth, TIntegrationSteps } from "components/integration";
// ui
import { PrimaryButton } from "components/ui";
// types
import { IAppIntegrations, IWorkspaceIntegrations } from "types";
type Props = {
provider: string | undefined;
handleStepChange: (value: TIntegrationSteps) => void;
appIntegrations: IAppIntegrations[] | undefined;
workspaceIntegrations: IWorkspaceIntegrations[] | undefined;
};
export const GithubImportConfigure: FC<Props> = ({
handleStepChange,
provider,
appIntegrations,
workspaceIntegrations,
}) => {
const router = useRouter();
const { workspaceSlug } = router.query;
// current integration from all the integrations available
const integration =
appIntegrations &&
appIntegrations.length > 0 &&
appIntegrations.find((i) => i.provider === provider);
// current integration from workspace integrations
const workspaceIntegration =
integration &&
workspaceIntegrations &&
workspaceIntegrations.length > 0 &&
workspaceIntegrations.find((i: any) => i.integration_detail.id === integration.id);
return (
<div className="space-y-6">
<div className="flex items-center gap-2 py-5">
<div className="w-full">
<div className="font-medium">Configure</div>
<div className="text-sm text-gray-600">Set up your GitHub import.</div>
</div>
<div className="flex-shrink-0">
<GithubAuth
workspaceSlug={workspaceSlug as string}
workspaceIntegration={workspaceIntegration}
/>
</div>
</div>
<div className="flex items-center justify-end">
<PrimaryButton
onClick={() => handleStepChange("import-data")}
disabled={workspaceIntegration && workspaceIntegration?.id ? false : true}
>
Next
</PrimaryButton>
</div>
</div>
);
};

View File

@ -0,0 +1,27 @@
import { FC } from "react";
// react-hook-form
import { UseFormWatch } from "react-hook-form";
// ui
import { PrimaryButton, SecondaryButton } from "components/ui";
// types
import { TFormValues, TIntegrationSteps } from "components/integration";
type Props = {
handleStepChange: (value: TIntegrationSteps) => void;
watch: UseFormWatch<TFormValues>;
};
export const GithubImportConfirm: FC<Props> = ({ handleStepChange, watch }) => (
<div className="mt-6">
<h4 className="font-medium">
You are about to import issues from {watch("github").full_name}. Click on {'"'}Confirm &
Import{'" '}
to complete the process.
</h4>
<div className="mt-6 flex items-center justify-between">
<SecondaryButton onClick={() => handleStepChange("import-users")}>Back</SecondaryButton>
<PrimaryButton type="submit">Confirm & Import</PrimaryButton>
</div>
</div>
);

View File

@ -1,27 +1,127 @@
import { FC } from "react";
import { FC, useState } from "react";
// react-hook-form
import { Control, Controller, UseFormWatch } from "react-hook-form";
// hooks
import useProjects from "hooks/use-projects";
// components
import { SelectRepository, TFormValues, TIntegrationSteps } from "components/integration";
// ui
import { CustomSearchSelect, PrimaryButton, SecondaryButton } from "components/ui";
// helpers
import { truncateText } from "helpers/string.helper";
// types
import { IIntegrationData } from "components/integration";
import { IWorkspaceIntegrations } from "types";
type Props = { state: IIntegrationData; handleState: (key: string, valve: any) => void };
type Props = {
handleStepChange: (value: TIntegrationSteps) => void;
integration: IWorkspaceIntegrations | false | undefined;
control: Control<TFormValues, any>;
watch: UseFormWatch<TFormValues>;
};
export const GithubImportData: FC<Props> = ({ state, handleState }) => (
<div>
<div>Import Data</div>
<div className="mt-5 flex items-center justify-between">
export const GithubImportData: FC<Props> = ({ handleStepChange, integration, control, watch }) => {
const { projects } = useProjects();
const options =
projects.map((project) => ({
value: project.id,
query: project.name,
content: <p>{truncateText(project.name, 25)}</p>,
})) ?? [];
return (
<div className="mt-6">
<div className="space-y-8">
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-8">
<h4 className="font-semibold">Select Repository</h4>
<p className="text-gray-500 text-xs">
Select the repository that you want the issues to be imported from.
</p>
</div>
<div className="col-span-12 sm:col-span-4">
{integration && (
<Controller
control={control}
name="github"
render={({ field: { value, onChange } }) => (
<SelectRepository
integration={integration}
value={value ? value.id : null}
label={value ? `${value.full_name}` : "Select Repository"}
onChange={onChange}
characterLimit={50}
/>
)}
/>
)}
</div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-8">
<h4 className="font-semibold">Select Project</h4>
<p className="text-gray-500 text-xs">Select the project to import the issues to.</p>
</div>
<div className="col-span-12 sm:col-span-4">
{projects && (
<Controller
control={control}
name="project"
render={({ field: { value, onChange } }) => (
<CustomSearchSelect
value={value}
label={value ? projects.find((p) => p.id === value)?.name : "Select Project"}
onChange={onChange}
options={options}
optionsClassName="w-full"
/>
)}
/>
)}
</div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-8">
<h4 className="font-semibold">Sync Issues</h4>
<p className="text-gray-500 text-xs">Set whether you want to sync the issues or not.</p>
</div>
<div className="col-span-12 sm:col-span-4">
<Controller
control={control}
name="sync"
render={({ field: { value, onChange } }) => (
<button
type="button"
className={`rounded-sm bg-gray-300 px-3 py-1.5 text-sm transition-colors hover:bg-opacity-80`}
onClick={() => handleState("state", "import-configure")}
className={`relative inline-flex h-3.5 w-6 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
value ? "bg-green-500" : "bg-gray-200"
}`}
role="switch"
aria-checked={value ? true : false}
onClick={() => onChange(!value)}
>
Back
<span className="sr-only">Show empty groups</span>
<span
aria-hidden="true"
className={`inline-block h-2.5 w-2.5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
value ? "translate-x-2.5" : "translate-x-0"
}`}
/>
</button>
<button
type="button"
className={`rounded-sm bg-theme px-3 py-1.5 text-sm text-white transition-colors hover:bg-opacity-80`}
onClick={() => handleState("state", "migrate-issues")}
)}
/>
</div>
</div>
</div>
<div className="mt-6 flex items-center justify-end gap-2">
<SecondaryButton onClick={() => handleStepChange("import-configure")}>Back</SecondaryButton>
<PrimaryButton
onClick={() => handleStepChange("repo-details")}
disabled={!watch("github") || !watch("project")}
>
Next
</button>
</PrimaryButton>
</div>
</div>
);
);
};

View File

@ -0,0 +1,59 @@
import { FC } from "react";
import { useRouter } from "next/router";
// react-hook-form
import { UseFormWatch } from "react-hook-form";
// ui
import { PrimaryButton, SecondaryButton } from "components/ui";
// types
import {
IUserDetails,
SingleUserSelect,
TFormValues,
TIntegrationSteps,
} from "components/integration";
type Props = {
handleStepChange: (value: TIntegrationSteps) => void;
users: IUserDetails[];
setUsers: React.Dispatch<React.SetStateAction<IUserDetails[]>>;
watch: UseFormWatch<TFormValues>;
};
export const GithubImportUsers: FC<Props> = ({ handleStepChange, users, setUsers, watch }) => {
const router = useRouter();
const isInvalid = users.filter((u) => u.import !== false && u.email === "").length > 0;
return (
<div className="mt-6">
<div>
<div className="grid grid-cols-3 gap-2 text-sm mb-2 font-medium">
<div>Name</div>
<div>Import as...</div>
<div className="text-right">
{users.filter((u) => u.import !== false).length} users selected
</div>
</div>
<div className="space-y-2">
{watch("collaborators").map((collaborator, index) => (
<SingleUserSelect
key={collaborator.id}
collaborator={collaborator}
index={index}
users={users}
setUsers={setUsers}
/>
))}
</div>
</div>
<div className="mt-6 flex items-center justify-end gap-2">
<SecondaryButton onClick={() => handleStepChange("repo-details")}>Back</SecondaryButton>
<PrimaryButton onClick={() => handleStepChange("import-confirm")} disabled={isInvalid}>
Next
</PrimaryButton>
</div>
</div>
);
};

View File

@ -1,27 +0,0 @@
import { FC } from "react";
// types
import { IIntegrationData } from "components/integration";
type Props = { state: IIntegrationData; handleState: (key: string, valve: any) => void };
export const GithubIssuesSelect: FC<Props> = ({ state, handleState }) => (
<div>
<div>Issues Select</div>
<div className="mt-5 flex items-center justify-between">
<button
type="button"
className={`rounded-sm bg-gray-300 px-3 py-1.5 text-sm transition-colors hover:bg-opacity-80`}
onClick={() => handleState("state", "import-import-data")}
>
Back
</button>
<button
type="button"
className={`rounded-sm bg-theme px-3 py-1.5 text-sm text-white transition-colors hover:bg-opacity-80`}
onClick={() => handleState("state", "migrate-users")}
>
Next
</button>
</div>
</div>
);

View File

@ -0,0 +1,105 @@
import { FC, useEffect } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// react-hook-form
import { UseFormSetValue } from "react-hook-form";
// services
import GithubIntegrationService from "services/integration/github.service";
// ui
import { Loader, PrimaryButton, SecondaryButton } from "components/ui";
// types
import { IUserDetails, TFormValues, TIntegrationSteps } from "components/integration";
// fetch-keys
import { GITHUB_REPOSITORY_INFO } from "constants/fetch-keys";
type Props = {
selectedRepo: any;
handleStepChange: (value: TIntegrationSteps) => void;
setUsers: React.Dispatch<React.SetStateAction<IUserDetails[]>>;
setValue: UseFormSetValue<TFormValues>;
};
export const GithubRepoDetails: FC<Props> = ({
selectedRepo,
handleStepChange,
setUsers,
setValue,
}) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { data: repoInfo } = useSWR(
workspaceSlug && selectedRepo
? GITHUB_REPOSITORY_INFO(workspaceSlug as string, selectedRepo.name)
: null,
workspaceSlug && selectedRepo
? () =>
GithubIntegrationService.getGithubRepoInfo(workspaceSlug as string, {
owner: selectedRepo.owner.login,
repo: selectedRepo.name,
})
: null
);
useEffect(() => {
if (!repoInfo) return;
setValue("collaborators", repoInfo.collaborators);
const fetchedUsers = repoInfo.collaborators.map((collaborator) => ({
username: collaborator.login,
import: "map",
email: "",
}));
setUsers(fetchedUsers);
}, [repoInfo, setUsers, setValue]);
return (
<div className="mt-6">
{repoInfo ? (
repoInfo.issue_count > 0 ? (
<div className="flex items-center justify-between gap-4">
<div>
<div className="font-medium">Repository Details</div>
<div className="text-sm text-gray-600">Import completed. We have found:</div>
</div>
<div className="flex gap-16 mt-4">
<div className="text-center flex-shrink-0">
<p className="text-3xl font-bold">{repoInfo.issue_count}</p>
<h6 className="text-sm text-gray-500">Issues</h6>
</div>
<div className="text-center flex-shrink-0">
<p className="text-3xl font-bold">{repoInfo.labels}</p>
<h6 className="text-sm text-gray-500">Labels</h6>
</div>
<div className="text-center flex-shrink-0">
<p className="text-3xl font-bold">{repoInfo.collaborators.length}</p>
<h6 className="text-sm text-gray-500">Users</h6>
</div>
</div>
</div>
) : (
<div>
<h5>We didn{"'"}t find any issue in this repository.</h5>
</div>
)
) : (
<Loader>
<Loader.Item height="70px" />
</Loader>
)}
<div className="mt-6 flex items-center justify-end gap-2">
<SecondaryButton onClick={() => handleStepChange("import-data")}>Back</SecondaryButton>
<PrimaryButton
onClick={() => handleStepChange("import-users")}
disabled={!repoInfo || repoInfo.issue_count === 0}
>
Next
</PrimaryButton>
</div>
</div>
);
};

View File

@ -1,48 +1,77 @@
import { FC, useState } from "react";
// next imports
import Link from "next/link";
import Image from "next/image";
// icons
import GithubLogo from "public/logos/github-square.png";
import { CogIcon, CloudUploadIcon, UsersIcon, ImportLayersIcon, CheckIcon } from "components/icons";
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
import { useRouter } from "next/router";
import { mutate } from "swr";
// react-hook-form
import { useForm } from "react-hook-form";
// services
import GithubIntegrationService from "services/integration/github.service";
// hooks
import useToast from "hooks/use-toast";
// components
import {
GithubConfigure,
GithubImportConfigure,
GithubImportData,
GithubIssuesSelect,
GithubUsersSelect,
GithubConfirm,
GithubRepoDetails,
GithubImportUsers,
GithubImportConfirm,
} from "components/integration";
// icons
import { CogIcon, CloudUploadIcon, UsersIcon, CheckIcon } from "components/icons";
import { ArrowLeftIcon, ListBulletIcon } from "@heroicons/react/24/outline";
// images
import GithubLogo from "public/logos/github-square.png";
// types
import { IAppIntegrations } from "types";
import {
IAppIntegrations,
IGithubRepoCollaborator,
IGithubServiceImportFormData,
IWorkspaceIntegrations,
} from "types";
// fetch-keys
import { IMPORTER_SERVICES_LIST } from "constants/fetch-keys";
type Props = {
workspaceSlug: string | undefined;
provider: string | undefined;
allIntegrations: IAppIntegrations[] | undefined;
allIntegrationsError: Error | undefined;
allWorkspaceIntegrations: any | undefined;
allWorkspaceIntegrationsError: Error | undefined;
allIntegrationImporters: any | undefined;
allIntegrationImportersError: Error | undefined;
appIntegrations: IAppIntegrations[] | undefined;
workspaceIntegrations: IWorkspaceIntegrations[] | undefined;
};
export type TIntegrationSteps =
| "import-configure"
| "import-data"
| "repo-details"
| "import-users"
| "import-confirm";
export interface IIntegrationData {
state: string;
state: TIntegrationSteps;
}
export const GithubIntegrationRoot: FC<Props> = ({
workspaceSlug,
provider,
allIntegrations,
allIntegrationsError,
allWorkspaceIntegrations,
allWorkspaceIntegrationsError,
allIntegrationImporters,
allIntegrationImportersError,
}) => {
const integrationWorkflowData = [
export interface IUserDetails {
username: string;
import: any;
email: string;
}
export type TFormValues = {
github: any;
project: string | null;
sync: boolean;
collaborators: IGithubRepoCollaborator[];
users: IUserDetails[];
};
const defaultFormValues = {
github: null,
project: null,
sync: false,
};
const integrationWorkflowData = [
{
title: "Configure",
key: "import-configure",
@ -50,80 +79,142 @@ export const GithubIntegrationRoot: FC<Props> = ({
},
{
title: "Import Data",
key: "import-import-data",
key: "import-data",
icon: CloudUploadIcon,
},
{ title: "Issues", key: "migrate-issues", icon: UsersIcon },
{ title: "Issues", key: "repo-details", icon: ListBulletIcon },
{
title: "Users",
key: "migrate-users",
icon: ImportLayersIcon,
key: "import-users",
icon: UsersIcon,
},
{
title: "Confirm",
key: "migrate-confirm",
key: "import-confirm",
icon: CheckIcon,
},
];
];
export const GithubIntegrationRoot: FC<Props> = ({
provider,
appIntegrations,
workspaceIntegrations,
}) => {
const [currentStep, setCurrentStep] = useState<IIntegrationData>({
state: "import-configure",
});
const [users, setUsers] = useState<IUserDetails[]>([]);
const router = useRouter();
const { workspaceSlug } = router.query;
const { setToastAlert } = useToast();
const { handleSubmit, control, setValue, watch } = useForm<TFormValues>({
defaultValues: defaultFormValues,
});
const activeIntegrationState = () => {
const currentElementIndex = integrationWorkflowData.findIndex(
(_item) => _item?.key === integrationData?.state
(i) => i?.key === currentStep?.state
);
return currentElementIndex;
};
const [integrationData, setIntegrationData] = useState<IIntegrationData>({
state: "import-configure",
});
const handleIntegrationData = (key: string = "state", value: string) => {
setIntegrationData((previousData) => ({ ...previousData, [key]: value }));
const handleStepChange = (value: TIntegrationSteps) => {
setCurrentStep((prevData) => ({ ...prevData, state: value }));
};
// current integration from all the integrations available
const integration =
appIntegrations &&
appIntegrations.length > 0 &&
appIntegrations.find((i) => i.provider === provider);
// current integration from workspace integrations
const workspaceIntegration =
integration &&
workspaceIntegrations?.find((i: any) => i.integration_detail.id === integration.id);
const createGithubImporterService = async (formData: TFormValues) => {
if (!formData.github || !formData.project) return;
const payload: IGithubServiceImportFormData = {
metadata: {
owner: formData.github.owner.login,
name: formData.github.name,
repository_id: formData.github.id,
url: formData.github.html_url,
},
data: {
users: users,
},
config: {
sync: formData.sync,
},
project_id: formData.project,
};
await GithubIntegrationService.createGithubServiceImport(workspaceSlug as string, payload)
.then(() => {
router.push(`/${workspaceSlug}/settings/import-export`);
mutate(IMPORTER_SERVICES_LIST(workspaceSlug as string));
})
.catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "Import was unsuccessful. Please try again.",
})
);
};
return (
<div className="space-y-4">
<form onSubmit={handleSubmit(createGithubImporterService)}>
<div className="space-y-2">
<Link href={`/${workspaceSlug}/settings/import-export`}>
<div className="inline-flex cursor-pointer items-center gap-2 text-sm font-medium text-gray-600 hover:text-gray-900">
<div>
<ArrowLeftIcon className="h-3 w-3" />
</div>
<div>Back</div>
<div>Cancel import & go back</div>
</div>
</Link>
<div className="space-y-4 rounded border border-gray-200 bg-white p-4">
<div className="space-y-4 rounded-[10px] border border-gray-200 bg-white p-4">
<div className="flex items-center gap-2">
<div className="h-10 w-10 flex-shrink-0">
<Image src={GithubLogo} alt="GithubLogo" />
</div>
<div className="flex h-full w-full items-center justify-center">
{integrationWorkflowData.map((_integration, _idx) => (
{integrationWorkflowData.map((integration, index) => (
<>
<div
key={_integration?.key}
className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full border
${
_idx <= activeIntegrationState()
key={integration.key}
className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full border ${
index <= activeIntegrationState()
? `border-[#3F76FF] bg-[#3F76FF] text-white ${
_idx === activeIntegrationState()
? `border-opacity-100 bg-opacity-100`
: `border-opacity-80 bg-opacity-80`
index === activeIntegrationState()
? "border-opacity-100 bg-opacity-100"
: "border-opacity-80 bg-opacity-80"
}`
: `border-gray-300`
}
`}
: "border-gray-300"
}`}
>
<_integration.icon
width={`18px`}
height={`18px`}
color={_idx <= activeIntegrationState() ? "#ffffff" : "#d1d5db"}
<integration.icon
width="18px"
height="18px"
color={index <= activeIntegrationState() ? "#ffffff" : "#d1d5db"}
/>
</div>
{_idx < integrationWorkflowData.length - 1 && (
{index < integrationWorkflowData.length - 1 && (
<div
key={_idx}
key={index}
className={`border-b px-7 ${
_idx <= activeIntegrationState() - 1 ? `border-[#3F76FF]` : `border-gray-300`
index <= activeIntegrationState() - 1
? `border-[#3F76FF]`
: `border-gray-300`
}`}
>
{" "}
@ -134,35 +225,47 @@ export const GithubIntegrationRoot: FC<Props> = ({
</div>
</div>
<div className="relative w-full space-y-4 overflow-hidden">
<div className="relative w-full space-y-4">
<div className="w-full">
{integrationData?.state === "import-configure" && (
<GithubConfigure
state={integrationData}
handleState={handleIntegrationData}
workspaceSlug={workspaceSlug}
{currentStep?.state === "import-configure" && (
<GithubImportConfigure
handleStepChange={handleStepChange}
provider={provider}
allIntegrations={allIntegrations}
allIntegrationsError={allIntegrationsError}
allWorkspaceIntegrations={allWorkspaceIntegrations}
allWorkspaceIntegrationsError={allWorkspaceIntegrationsError}
appIntegrations={appIntegrations}
workspaceIntegrations={workspaceIntegrations}
/>
)}
{integrationData?.state === "import-import-data" && (
<GithubImportData state={integrationData} handleState={handleIntegrationData} />
{currentStep?.state === "import-data" && (
<GithubImportData
handleStepChange={handleStepChange}
integration={workspaceIntegration}
control={control}
watch={watch}
/>
)}
{integrationData?.state === "migrate-issues" && (
<GithubIssuesSelect state={integrationData} handleState={handleIntegrationData} />
{currentStep?.state === "repo-details" && (
<GithubRepoDetails
selectedRepo={watch("github")}
handleStepChange={handleStepChange}
setUsers={setUsers}
setValue={setValue}
/>
)}
{integrationData?.state === "migrate-users" && (
<GithubUsersSelect state={integrationData} handleState={handleIntegrationData} />
{currentStep?.state === "import-users" && (
<GithubImportUsers
handleStepChange={handleStepChange}
users={users}
setUsers={setUsers}
watch={watch}
/>
)}
{integrationData?.state === "migrate-confirm" && (
<GithubConfirm state={integrationData} handleState={handleIntegrationData} />
{currentStep?.state === "import-confirm" && (
<GithubImportConfirm handleStepChange={handleStepChange} watch={watch} />
)}
</div>
</div>
</div>
</div>
</form>
);
};

View File

@ -0,0 +1,95 @@
import React from "react";
import { useRouter } from "next/router";
import useSWRInfinite from "swr/infinite";
// services
import projectService from "services/project.service";
// ui
import { CustomSearchSelect } from "components/ui";
// helpers
import { truncateText } from "helpers/string.helper";
// types
import { IWorkspaceIntegrations } from "types";
type Props = {
integration: IWorkspaceIntegrations;
value: any;
label: string;
onChange: (repo: any) => void;
characterLimit?: number;
};
export const SelectRepository: React.FC<Props> = ({
integration,
value,
label,
onChange,
characterLimit = 25,
}) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const getKey = (pageIndex: number) => {
if (!workspaceSlug || !integration) return;
return `${
process.env.NEXT_PUBLIC_API_BASE_URL
}/api/workspaces/${workspaceSlug}/workspace-integrations/${
integration.id
}/github-repositories/?page=${++pageIndex}`;
};
const fetchGithubRepos = async (url: string) => {
const data = await projectService.getGithubRepositories(url);
return data;
};
const {
data: paginatedData,
size,
setSize,
isValidating,
} = useSWRInfinite(getKey, fetchGithubRepos);
const userRepositories = (paginatedData ?? []).map((data) => data.repositories).flat();
const totalCount = paginatedData && paginatedData.length > 0 ? paginatedData[0].total_count : 0;
const options =
userRepositories.map((repo) => ({
value: repo.id,
query: repo.full_name,
content: <p>{truncateText(repo.full_name, characterLimit)}</p>,
})) ?? [];
return (
<CustomSearchSelect
value={value}
options={options}
onChange={(val: string) => {
const repo = userRepositories.find((repo) => repo.id === val);
onChange(repo);
}}
label={label}
footerOption={
<>
{userRepositories && options.length < totalCount && (
<button
type="button"
className="w-full p-1 text-center text-[0.6rem] text-gray-500 hover:bg-hover-gray"
onClick={() => setSize(size + 1)}
disabled={isValidating}
>
{isValidating ? "Loading..." : "Click to load more..."}
</button>
)}
</>
}
position="right"
optionsClassName="w-full"
/>
);
};

View File

@ -0,0 +1,133 @@
import Image from "next/image";
import { useRouter } from "next/router";
import useSWR from "swr";
// services
import workspaceService from "services/workspace.service";
// ui
import { Avatar, CustomSearchSelect, CustomSelect, Input } from "components/ui";
// types
import { IGithubRepoCollaborator } from "types";
import { IUserDetails } from "./root";
// fetch-keys
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
type Props = {
collaborator: IGithubRepoCollaborator;
index: number;
users: IUserDetails[];
setUsers: React.Dispatch<React.SetStateAction<IUserDetails[]>>;
};
const importOptions = [
{
key: "map",
label: "Map to existing",
},
{
key: "invite",
label: "Invite by email",
},
{
key: false,
label: "Do not import",
},
];
export const SingleUserSelect: React.FC<Props> = ({ collaborator, index, users, setUsers }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { data: members } = useSWR(
workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug as string) : null,
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug as string) : null
);
const options =
members?.map((member) => ({
value: member.member.email,
query:
(member.member.first_name && member.member.first_name !== ""
? member.member.first_name
: member.member.email) +
" " +
member.member.last_name ?? "",
content: (
<div className="flex items-center gap-2">
<Avatar user={member.member} />
{member.member.first_name && member.member.first_name !== ""
? member.member.first_name + "(" + member.member.email + ")"
: member.member.email}
</div>
),
})) ?? [];
return (
<div className="bg-gray-50 px-2 py-3 rounded-md grid grid-cols-3 items-center gap-2">
<div className="flex items-center gap-2">
<div className="relative h-8 w-8 rounded flex-shrink-0">
<Image
src={collaborator.avatar_url}
layout="fill"
objectFit="cover"
className="rounded"
alt={`${collaborator.login} GitHub user`}
/>
</div>
<p className="text-sm">{collaborator.login}</p>
</div>
<div>
<CustomSelect
value={users[index].import}
label={
<div className="text-xs">
{importOptions.find((o) => o.key === users[index].import)?.label}
</div>
}
onChange={(val: any) => {
const newUsers = [...users];
newUsers[index].import = val;
newUsers[index].email = "";
setUsers(newUsers);
}}
optionsClassName="w-full"
noChevron
>
{importOptions.map((option) => (
<CustomSelect.Option key={option.label} value={option.key}>
<div>{option.label}</div>
</CustomSelect.Option>
))}
</CustomSelect>
</div>
{users[index].import === "invite" && (
<Input
type="email"
name={`userEmail${index}`}
value={users[index].email}
onChange={(e) => {
const newUsers = [...users];
newUsers[index].email = e.target.value;
setUsers(newUsers);
}}
placeholder="Enter email of the user"
className="py-1 border-gray-200 text-xs"
/>
)}
{users[index].import === "map" && members && (
<CustomSearchSelect
value={users[index].email}
label={users[index].email !== "" ? users[index].email : "Select user from project"}
options={options}
onChange={(val: string) => {
const newUsers = [...users];
newUsers[index].email = val;
setUsers(newUsers);
}}
optionsClassName="w-full"
/>
)}
</div>
);
};

View File

@ -1,27 +0,0 @@
import { FC } from "react";
// types
import { IIntegrationData } from "components/integration";
type Props = { state: IIntegrationData; handleState: (key: string, valve: any) => void };
export const GithubUsersSelect: FC<Props> = ({ state, handleState }) => (
<div>
<div>Users Select</div>
<div className="mt-5 flex items-center justify-between">
<button
type="button"
className={`rounded-sm bg-gray-300 px-3 py-1.5 text-sm transition-colors hover:bg-opacity-80`}
onClick={() => handleState("state", "migrate-issues")}
>
Back
</button>
<button
type="button"
className={`rounded-sm bg-theme px-3 py-1.5 text-sm text-white transition-colors hover:bg-opacity-80`}
onClick={() => handleState("state", "migrate-confirm")}
>
Next
</button>
</div>
</div>
);

View File

@ -1,49 +1,60 @@
import { FC } from "react";
// next imports
import { FC, useState } from "react";
import Link from "next/link";
import Image from "next/image";
import { useRouter } from "next/router";
import { mutate } from "swr";
// icons
import { ArrowRightIcon } from "components/icons";
import GithubLogo from "public/logos/github-square.png";
import { ChevronDownIcon } from "@heroicons/react/24/outline";
// components
import { Loader } from "components/ui";
import { GithubIntegrationRoot } from "components/integration";
// icons
import { ArrowPathIcon } from "@heroicons/react/24/outline";
// ui
import { Loader, PrimaryButton, SecondaryButton } from "components/ui";
// images
import GithubLogo from "public/logos/github-square.png";
// helpers
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
// types
import { IAppIntegrations } from "types";
import { IAppIntegrations, IImporterService, IWorkspaceIntegrations } from "types";
// fetch-keys
import { IMPORTER_SERVICES_LIST } from "constants/fetch-keys";
type Props = {
workspaceSlug: string | undefined;
provider: string | undefined;
allIntegrations: IAppIntegrations[] | undefined;
allIntegrationsError: Error | undefined;
allWorkspaceIntegrations: any | undefined;
allWorkspaceIntegrationsError: Error | undefined;
allIntegrationImporters: any | undefined;
allIntegrationImportersError: Error | undefined;
appIntegrations: IAppIntegrations[] | undefined;
workspaceIntegrations: IWorkspaceIntegrations[] | undefined;
importerServices: IImporterService[] | undefined;
};
const importersList: { [key: string]: string } = {
github: "GitHub",
};
const IntegrationGuide: FC<Props> = ({
workspaceSlug,
provider,
allIntegrations,
allIntegrationsError,
allWorkspaceIntegrations,
allWorkspaceIntegrationsError,
allIntegrationImporters,
allIntegrationImportersError,
}) => (
appIntegrations,
workspaceIntegrations,
importerServices,
}) => {
const [refreshing, setRefreshing] = useState(false);
const router = useRouter();
const { workspaceSlug } = router.query;
return (
<div className="space-y-5">
{!provider && (
<>
<div className="text-2xl font-semibold">Import</div>
<div className="flex items-center gap-2">
<div className="h-full w-full space-y-1">
<div className="text-lg font-medium">Relocation Guide</div>
<div className="text-sm">
You can now transfer all the issues that youve created in other tracking services.
This tool will guide you to relocate the issue to Plane.
You can now transfer all the issues that you{"'"}ve created in other tracking
services. This tool will guide you to relocate the issue to Plane.
</div>
</div>
<div className="flex flex-shrink-0 cursor-pointer items-center gap-2 text-sm font-medium text-[#3F76FF] hover:text-opacity-80">
@ -53,110 +64,135 @@ const IntegrationGuide: FC<Props> = ({
</div>
</div>
</div>
<div>
{allIntegrations && !allIntegrationsError ? (
<>
{allIntegrations && allIntegrations.length > 0 ? (
<>
{allIntegrations.map((_integration, _idx) => (
<div
key={_idx}
className="space-y-4 rounded border border-gray-200 bg-white p-4"
>
{appIntegrations ? (
appIntegrations.length > 0 ? (
appIntegrations.map((integration, index) => (
<div key={index} className="rounded-[10px] border bg-white p-4">
<div className="flex items-center gap-4 whitespace-nowrap">
<div className="h-[40px] w-[40px] flex-shrink-0">
{_integration?.provider === "github" && (
{integration?.provider === "github" && (
<Image src={GithubLogo} alt="GithubLogo" />
)}
</div>
<div className="w-full space-y-1">
<div className="flex items-center gap-2 font-medium">
<div>{_integration?.title}</div>
<div className="rounded-full border border-gray-200 bg-gray-200 px-3 text-[12px]">
0
</div>
<h3>{integration?.title}</h3>
</div>
<div className="text-sm text-gray-500">
Activate GitHub integrations on individual projects to sync with
specific repositories.
Activate GitHub integrations on individual projects to sync with specific
repositories.
</div>
</div>
<div className="flex-shrink-0">
<Link href={`/${workspaceSlug}/settings/import-export?provider=github`}>
<button
type="button"
className="w-full rounded bg-[#3F76FF] py-1.5 px-4 text-center text-sm text-white hover:bg-opacity-90"
>
Integrate Now
</button>
<PrimaryButton>Integrate Now</PrimaryButton>
</Link>
</div>
<div className="flex h-[24px] w-[24px] flex-shrink-0 cursor-pointer items-center justify-center rounded-sm bg-gray-100 hover:bg-gray-200">
<ChevronDownIcon className="h-4 w-4" />
</div>
</div>
<div>
{allIntegrationImporters && !allIntegrationImportersError ? (
<>
{allIntegrationImporters && allIntegrationImporters.length > 0 ? (
<></>
<h3 className="mt-6 mb-2 font-medium text-lg flex gap-2">
Previous Imports
<button
type="button"
className="flex-shrink-0 flex items-center gap-1 outline-none text-xs py-1 px-1.5 bg-gray-100 rounded"
onClick={() => {
setRefreshing(true);
mutate(IMPORTER_SERVICES_LIST(workspaceSlug as string)).then(() =>
setRefreshing(false)
);
}}
>
<ArrowPathIcon className={`h-3 w-3 ${refreshing ? "animate-spin" : ""}`} />{" "}
{refreshing ? "Refreshing..." : "Refresh status"}
</button>
</h3>
{importerServices ? (
importerServices.length > 0 ? (
<div className="space-y-2">
<div className="divide-y">
{importerServices.map((service) => (
<div key={service.id} className="py-3">
<h4 className="text-sm flex items-center gap-2">
<span>
Import from{" "}
<span className="font-medium">
{importersList[service.service]}
</span>{" "}
to{" "}
<span className="font-medium">
{service.project_detail.name}
</span>
</span>
<span
className={`capitalize px-2 py-0.5 text-xs rounded ${
service.status === "completed"
? "bg-green-100 text-green-500"
: service.status === "processing"
? "bg-yellow-100 text-yellow-500"
: service.status === "failed"
? "bg-red-100 text-red-500"
: ""
}`}
>
{refreshing ? "Refreshing..." : service.status}
</span>
</h4>
<div className="text-gray-500 text-xs mt-2 flex items-center gap-2">
<span>{renderShortDateWithYearFormat(service.created_at)}</span>|
<span>
Imported by{" "}
{service.initiated_by_detail.first_name &&
service.initiated_by_detail.first_name !== ""
? service.initiated_by_detail.first_name +
" " +
service.initiated_by_detail.last_name
: service.initiated_by_detail.email}
</span>
</div>
</div>
))}
</div>
</div>
) : (
<div className="py-2 text-sm text-gray-800">
Previous Imports not available.
No previous imports available.
</div>
)}
</>
)
) : (
<div>
<Loader className="grid grid-cols-1 gap-3">
{["", ""].map((_integration, _idx) => (
<Loader className="grid grid-cols-1 gap-3 mt-6">
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />
))}
</Loader>
</div>
)}
</div>
</div>
))}
</>
))
) : (
<div className="py-5 text-center text-sm text-gray-800">
Integrations not available.
</div>
)}
</>
)
) : (
<div>
<Loader className="grid grid-cols-1 gap-3">
{["", ""].map((_integration, _idx) => (
<Loader.Item height="34px" width="100%" />
))}
<Loader.Item height="34px" width="100%" />
</Loader>
</div>
)}
</div>
</>
)}
{provider && (
<>
{provider === "github" && (
{provider && provider === "github" && (
<GithubIntegrationRoot
workspaceSlug={workspaceSlug}
provider={provider}
allIntegrations={allIntegrations}
allIntegrationsError={allIntegrationsError}
allWorkspaceIntegrations={allWorkspaceIntegrations}
allWorkspaceIntegrationsError={allWorkspaceIntegrationsError}
allIntegrationImporters={allIntegrationImporters}
allIntegrationImportersError={allIntegrationImportersError}
appIntegrations={appIntegrations}
workspaceIntegrations={workspaceIntegrations}
/>
)}
</>
)}
</div>
);
);
};
export default IntegrationGuide;

View File

@ -7,8 +7,10 @@ export * from "./github/auth";
// layout
export * from "./github/root";
// components
export * from "./github/configure";
export * from "./github/import-configure";
export * from "./github/import-data";
export * from "./github/issues-select";
export * from "./github/users-select";
export * from "./github/confirm";
export * from "./github/repo-details";
export * from "./github/import-users";
export * from "./github/import-confirm";
export * from "./github/select-repository";
export * from "./github/single-user-select";

View File

@ -6,7 +6,7 @@ import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
// services
import workspaceService from "services/workspace.service";
import IntegrationService from "services/integration";
// hooks
import useToast from "hooks/use-toast";
// ui
@ -56,7 +56,9 @@ const OAuthPopUp = ({ integration }: any) => {
const { data: workspaceIntegrations } = useSWR(
workspaceSlug ? WORKSPACE_INTEGRATIONS(workspaceSlug as string) : null,
() =>
workspaceSlug ? workspaceService.getWorkspaceIntegrations(workspaceSlug as string) : null
workspaceSlug
? IntegrationService.getWorkspaceIntegrationsList(workspaceSlug as string)
: null
);
const handleRemoveIntegration = async () => {
@ -68,8 +70,10 @@ const OAuthPopUp = ({ integration }: any) => {
setDeletingIntegration(true);
await workspaceService
.deleteWorkspaceIntegration(workspaceSlug as string, workspaceIntegrationId ?? "")
await IntegrationService.deleteWorkspaceIntegration(
workspaceSlug as string,
workspaceIntegrationId ?? ""
)
.then(() => {
mutate<IWorkspaceIntegrations[]>(
WORKSPACE_INTEGRATIONS(workspaceSlug as string),

View File

@ -1,22 +1,18 @@
import React, { useState } from "react";
import React from "react";
import Image from "next/image";
import useSWR, { mutate } from "swr";
import useSWRInfinite from "swr/infinite";
// headless ui
import { Combobox, Transition } from "@headlessui/react";
// services
import projectService from "services/project.service";
// hooks
import { useRouter } from "next/router";
import useToast from "hooks/use-toast";
// components
import { SelectRepository } from "components/integration";
// icons
import { CheckIcon, ChevronDownIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import GithubLogo from "public/logos/github-square.png";
// helpers
import { truncateText } from "helpers/string.helper";
// types
import { IWorkspaceIntegrations } from "types";
// fetch-keys
@ -27,8 +23,6 @@ type Props = {
};
export const SingleIntegration: React.FC<Props> = ({ integration }) => {
const [query, setQuery] = useState("");
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@ -46,29 +40,6 @@ export const SingleIntegration: React.FC<Props> = ({ integration }) => {
: null
);
const getKey = (pageIndex: number) => {
if (!workspaceSlug || !integration) return;
return `${
process.env.NEXT_PUBLIC_API_BASE_URL
}/api/workspaces/${workspaceSlug}/workspace-integrations/${
integration.id
}/github-repositories/?page=${++pageIndex}`;
};
const fetchGithubRepos = async (url: string) => {
const data = await projectService.getGithubRepositories(url);
return data;
};
const {
data: paginatedData,
size,
setSize,
isValidating,
} = useSWRInfinite(getKey, fetchGithubRepos);
const handleChange = (repo: any) => {
if (!workspaceSlug || !projectId || !integration) return;
@ -107,21 +78,6 @@ export const SingleIntegration: React.FC<Props> = ({ integration }) => {
});
};
const userRepositories = (paginatedData ?? []).map((data) => data.repositories).flat();
const totalCount = paginatedData && paginatedData.length > 0 ? paginatedData[0].total_count : 0;
const options =
userRepositories.map((repo) => ({
value: repo.id,
query: repo.full_name,
content: <p>{truncateText(repo.full_name, 25)}</p>,
})) ?? [];
const filteredOptions =
query === ""
? options
: options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
return (
<>
{integration && (
@ -137,95 +93,20 @@ export const SingleIntegration: React.FC<Props> = ({ integration }) => {
<p className="text-sm text-gray-400">Select GitHub repository to enable sync.</p>
</div>
</div>
<Combobox
as="div"
<SelectRepository
integration={integration}
value={
syncedGithubRepository && syncedGithubRepository.length > 0
? `${syncedGithubRepository[0].repo_detail.owner}/${syncedGithubRepository[0].repo_detail.name}`
: null
}
onChange={(val: string) => {
const repo = userRepositories.find((repo) => repo.id === val);
handleChange(repo);
}}
className="relative flex-shrink-0 text-left"
>
{({ open }: any) => (
<>
<Combobox.Button className="flex w-full cursor-pointer items-center justify-between gap-1 rounded-md border px-3 py-1.5 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
{syncedGithubRepository && syncedGithubRepository.length > 0
label={
syncedGithubRepository && syncedGithubRepository.length > 0
? `${syncedGithubRepository[0].repo_detail.owner}/${syncedGithubRepository[0].repo_detail.name}`
: "Select Repository"}
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</Combobox.Button>
<Transition
show={open}
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Combobox.Options className="absolute right-0 z-10 mt-1 min-w-[10rem] origin-top-right rounded-md bg-white p-2 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="flex w-full items-center justify-start rounded-sm border bg-gray-100 px-2 text-gray-500">
<MagnifyingGlassIcon className="h-3 w-3" />
<Combobox.Input
className="w-full bg-transparent py-1 px-2 text-xs focus:outline-none"
onChange={(e) => setQuery(e.target.value)}
placeholder="Type to search..."
displayValue={(assigned: any) => assigned?.name}
/>
</div>
<div className="vertical-scroll-enable mt-2 max-h-44 space-y-1 overflow-y-scroll">
<p className="px-1 text-[0.6rem] text-gray-500">
{options.length} of {totalCount} repositories
</p>
{paginatedData ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ active, selected }) =>
`${active || selected ? "bg-hover-gray" : ""} ${
selected ? "font-medium" : ""
} flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 text-gray-500`
: "Select Repository"
}
>
{({ selected }) => (
<>
{option.content}
{selected && <CheckIcon className="h-4 w-4" />}
</>
)}
</Combobox.Option>
))
) : (
<p className="text-center text-gray-500">No matching results</p>
)
) : (
<p className="text-center text-gray-500">Loading...</p>
)}
{userRepositories && options.length < totalCount && (
<button
type="button"
className="w-full p-1 text-center text-[0.6rem] text-gray-500 hover:bg-hover-gray"
onClick={() => setSize(size + 1)}
disabled={isValidating}
>
{isValidating ? "Loading..." : "Click to load more..."}
</button>
)}
</div>
</Combobox.Options>
</Transition>
</>
)}
</Combobox>
onChange={handleChange}
/>
</div>
)}
</>

View File

@ -134,6 +134,7 @@ export const CustomSearchSelect = ({
{({ active, selected }) => (
<>
{option.content}
{multiple ? (
<div
className={`flex items-center justify-center rounded border border-gray-500 p-0.5 ${
active || selected ? "opacity-100" : "opacity-0"
@ -143,6 +144,11 @@ export const CustomSearchSelect = ({
className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`}
/>
</div>
) : (
<CheckIcon
className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`}
/>
)}
</>
)}
</Combobox.Option>

View File

@ -23,12 +23,9 @@ const paramsToKey = (params: any) => {
export const CURRENT_USER = "CURRENT_USER";
export const USER_WORKSPACE_INVITATIONS = "USER_WORKSPACE_INVITATIONS";
export const USER_WORKSPACES = "USER_WORKSPACES";
export const APP_INTEGRATIONS = "APP_INTEGRATIONS";
export const WORKSPACE_DETAILS = (workspaceSlug: string) =>
`WORKSPACE_DETAILS_${workspaceSlug.toUpperCase()}`;
export const WORKSPACE_INTEGRATIONS = (workspaceSlug: string) =>
`WORKSPACE_INTEGRATIONS_${workspaceSlug.toUpperCase()}`;
export const WORKSPACE_MEMBERS = (workspaceSlug: string) =>
`WORKSPACE_MEMBERS_${workspaceSlug.toUpperCase()}`;
@ -120,6 +117,17 @@ export const ISSUE_DETAILS = (issueId: string) => `ISSUE_DETAILS_${issueId.toUpp
export const SUB_ISSUES = (issueId: string) => `SUB_ISSUES_${issueId.toUpperCase()}`;
// integrations
export const APP_INTEGRATIONS = "APP_INTEGRATIONS";
export const WORKSPACE_INTEGRATIONS = (workspaceSlug: string) =>
`WORKSPACE_INTEGRATIONS_${workspaceSlug.toUpperCase()}`;
//import-export
export const IMPORTER_SERVICES_LIST = (workspaceSlug: string) =>
`IMPORTER_SERVICES_LIST_${workspaceSlug.toUpperCase()}`;
// github-importer
export const GITHUB_REPOSITORY_INFO = (workspaceSlug: string, repoName: string) =>
`GITHUB_REPO_INFO_${workspaceSlug.toString().toUpperCase()}_${repoName.toUpperCase()}`;
// Calendar
export const PROJECT_CALENDAR_ISSUES = (projectId: string) =>

View File

@ -29,10 +29,10 @@ const SettingsNavbar: React.FC<Props> = ({ profilePage = false }) => {
label: "Integrations",
href: `/${workspaceSlug}/settings/integrations`,
},
// {
// label: "Import/Export",
// href: `/${workspaceSlug}/settings/import-export`,
// },
{
label: "Import/Export",
href: `/${workspaceSlug}/settings/import-export`,
},
];
const projectLinks: Array<{

View File

@ -10,6 +10,7 @@ const nextConfig = {
"planefs-staging.s3.ap-south-1.amazonaws.com",
"planefs.s3.amazonaws.com",
"images.unsplash.com",
"avatars.githubusercontent.com",
],
},
output: "standalone",

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React from "react";
import { useRouter } from "next/router";
@ -9,8 +9,10 @@ import { requiredAdmin } from "lib/auth";
// layouts
import AppLayout from "layouts/app-layout";
// services
import workspaceService from "services/workspace.service";
import IntegrationService from "services/integration";
import projectService from "services/project.service";
// components
import { SingleIntegration } from "components/project";
// ui
import { EmptySpace, EmptySpaceItem, Loader } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
@ -21,7 +23,6 @@ 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";
const ProjectIntegrations: NextPage<UserAuth> = (props) => {
const { isMember, isOwner, isViewer, isGuest } = props;
@ -39,7 +40,9 @@ const ProjectIntegrations: NextPage<UserAuth> = (props) => {
const { data: workspaceIntegrations } = useSWR(
workspaceSlug ? WORKSPACE_INTEGRATIONS(workspaceSlug as string) : null,
() =>
workspaceSlug ? workspaceService.getWorkspaceIntegrations(workspaceSlug as string) : null
workspaceSlug
? IntegrationService.getWorkspaceIntegrationsList(workspaceSlug as string)
: null
);
return (

View File

@ -1,10 +1,11 @@
// next imports
import type { GetServerSideProps, NextPage } from "next";
import { useRouter } from "next/router";
// swr
import useSWR from "swr";
// lib
import { requiredWorkspaceAdmin } from "lib/auth";
// services
import IntegrationService from "services/integration";
// hooks
import useToast from "hooks/use-toast";
// layouts
@ -13,52 +14,38 @@ import IntegrationGuide from "components/integration/guide";
// ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// types
import { UserAuth, IAppIntegrations } from "types";
// api services
import WorkspaceIntegrationService from "services/integration";
import { UserAuth } from "types";
import type { GetServerSideProps, NextPage } from "next";
// fetch-keys
import {
APP_INTEGRATIONS,
IMPORTER_SERVICES_LIST,
WORKSPACE_INTEGRATIONS,
} from "constants/fetch-keys";
const ImportExport: NextPage<UserAuth> = (props) => {
const { setToastAlert } = useToast();
const router = useRouter();
const { workspaceSlug, provider } = router.query as {
workspaceSlug: string;
provider: string;
};
const { workspaceSlug, provider } = router.query;
// fetching all the integrations available
const { data: allIntegrations, error: allIntegrationsError } = useSWR<
IAppIntegrations[] | undefined,
Error
>(
workspaceSlug ? `ALL_INTEGRATIONS_${workspaceSlug.toUpperCase()}` : null,
workspaceSlug ? () => WorkspaceIntegrationService.listAllIntegrations() : null
const { data: appIntegrations } = useSWR(APP_INTEGRATIONS, () =>
IntegrationService.getAppIntegrationsList()
);
// fetching all the integrations available
const { data: allWorkspaceIntegrations, error: allWorkspaceIntegrationsError } = useSWR<
any | undefined,
Error
>(
workspaceSlug ? `WORKSPACE_INTEGRATIONS_${workspaceSlug.toUpperCase()}` : null,
const { data: workspaceIntegrations } = useSWR(
workspaceSlug ? WORKSPACE_INTEGRATIONS(workspaceSlug as string) : null,
workspaceSlug
? () => WorkspaceIntegrationService.listWorkspaceIntegrations(workspaceSlug)
? () => IntegrationService.getWorkspaceIntegrationsList(workspaceSlug as string)
: null
);
// fetching list of importers that already initialized
const { data: allIntegrationImporters, error: allIntegrationImportersError } = useSWR<
any | undefined,
Error
>(
workspaceSlug ? `INTEGRATION_IMPORTERS_${workspaceSlug.toUpperCase()}` : null,
workspaceSlug
? () => WorkspaceIntegrationService.fetchImportExportIntegrationStatus(workspaceSlug)
: null
const { data: importerServices } = useSWR(
workspaceSlug ? IMPORTER_SERVICES_LIST(workspaceSlug as string) : null,
workspaceSlug ? () => IntegrationService.getImporterServicesList(workspaceSlug as string) : null
);
return (
<>
<AppLayout
memberType={props}
breadcrumbs={
@ -69,20 +56,13 @@ const ImportExport: NextPage<UserAuth> = (props) => {
}
settingsLayout
>
<section className="space-y-5">
<IntegrationGuide
workspaceSlug={workspaceSlug}
provider={provider}
allIntegrations={allIntegrations}
allIntegrationsError={allIntegrationsError}
allWorkspaceIntegrations={allWorkspaceIntegrations}
allWorkspaceIntegrationsError={allWorkspaceIntegrationsError}
allIntegrationImporters={allIntegrationImporters}
allIntegrationImportersError={allIntegrationImportersError}
provider={provider as string}
appIntegrations={appIntegrations}
workspaceIntegrations={workspaceIntegrations}
importerServices={importerServices}
/>
</section>
</AppLayout>
</>
);
};

View File

@ -6,6 +6,7 @@ import useSWR from "swr";
// services
import workspaceService from "services/workspace.service";
import IntegrationService from "services/integration";
// lib
import { requiredWorkspaceAdmin } from "lib/auth";
// layouts
@ -30,8 +31,8 @@ const WorkspaceIntegrations: NextPage<UserAuth> = (props) => {
() => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null)
);
const { data: integrations } = useSWR(workspaceSlug ? APP_INTEGRATIONS : null, () =>
workspaceSlug ? workspaceService.getIntegrations() : null
const { data: appIntegrations } = useSWR(workspaceSlug ? APP_INTEGRATIONS : null, () =>
workspaceSlug ? IntegrationService.getAppIntegrationsList() : null
);
return (
@ -52,8 +53,8 @@ const WorkspaceIntegrations: NextPage<UserAuth> = (props) => {
<section className="space-y-8">
<h3 className="text-2xl font-semibold">Integrations</h3>
<div className="space-y-5">
{integrations ? (
integrations.map((integration) => (
{appIntegrations ? (
appIntegrations.map((integration) => (
<OAuthPopUp
key={integration.id}
workspaceSlug={workspaceSlug}

View File

@ -1,4 +1,5 @@
import APIService from "services/api.service";
import { IGithubRepoInfo, IGithubServiceImportFormData } from "types";
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
@ -9,7 +10,6 @@ class GithubIntegrationService extends APIService {
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
}
// fetching all the repositories under the github
async listAllRepositories(workspaceSlug: string, integrationSlug: string): Promise<any> {
return this.get(
`/api/workspaces/${workspaceSlug}/workspace-integrations/${integrationSlug}/github-repositories`
@ -20,21 +20,27 @@ class GithubIntegrationService extends APIService {
});
}
// fetching repository stats under the repository eg: users, labels and issues
async fetchRepositoryStats(workspaceSlug: string): Promise<any> {
return this.get(`/api/workspaces/${workspaceSlug}/importers/${integrationServiceType}/`)
async getGithubRepoInfo(
workspaceSlug: string,
params: { owner: string; repo: string }
): Promise<IGithubRepoInfo> {
return this.get(`/api/workspaces/${workspaceSlug}/importers/${integrationServiceType}/`, {
params,
})
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
// migrating repository data in workspace project
async migrateRepositoryStatToProject(
async createGithubServiceImport(
workspaceSlug: string,
integrationSlug: string
data: IGithubServiceImportFormData
): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/importers/${integrationServiceType}/`)
return this.post(
`/api/workspaces/${workspaceSlug}/projects/importers/${integrationServiceType}/`,
data
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;

View File

@ -1,16 +1,15 @@
import APIService from "services/api.service";
// types
import { IAppIntegrations, IWorkspaceIntegrations, IProject } from "types";
import { IAppIntegrations, IImporterService, IWorkspaceIntegrations } from "types";
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
class WorkspaceIntegrationService extends APIService {
class IntegrationService extends APIService {
constructor() {
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
}
// integration available and integration validation starts
async listAllIntegrations(): Promise<IAppIntegrations[]> {
async getAppIntegrationsList(): Promise<IAppIntegrations[]> {
return this.get(`/api/integrations/`)
.then((response) => response?.data)
.catch((error) => {
@ -18,26 +17,25 @@ class WorkspaceIntegrationService extends APIService {
});
}
async listWorkspaceIntegrations(workspaceSlug: string): Promise<IWorkspaceIntegrations[]> {
async getWorkspaceIntegrationsList(workspaceSlug: string): Promise<IWorkspaceIntegrations[]> {
return this.get(`/api/workspaces/${workspaceSlug}/workspace-integrations/`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
// integration available and integration validation ends
// listing all the projects under the workspace
async listWorkspaceProjects(workspaceSlug: string): Promise<IProject[]> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/`)
.then((response) => 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;
});
}
// fetching the status of all the importers that initiated eg: GitHub...
async fetchImportExportIntegrationStatus(workspaceSlug: string): Promise<any> {
async getImporterServicesList(workspaceSlug: string): Promise<IImporterService[]> {
return this.get(`/api/workspaces/${workspaceSlug}/importers/`)
.then((response) => response?.data)
.catch((error) => {
@ -46,4 +44,4 @@ class WorkspaceIntegrationService extends APIService {
}
}
export default new WorkspaceIntegrationService();
export default new IntegrationService();

View File

@ -10,8 +10,6 @@ import {
IWorkspaceMember,
IWorkspaceMemberInvitation,
ILastActiveWorkspaceDetails,
IAppIntegrations,
IWorkspaceIntegrations,
IWorkspaceSearchResults,
} from "types";
@ -195,32 +193,6 @@ class WorkspaceService extends APIService {
});
}
async getIntegrations(): Promise<IAppIntegrations[]> {
return this.get(`/api/integrations/`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
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;
});
}
async searchWorkspace(
workspaceSlug: string,
projectId: string,

View File

@ -0,0 +1,33 @@
export interface IGithubServiceImportFormData {
metadata: {
owner: string;
name: string;
repository_id: number;
url: string;
};
data: {
users: {
username: string;
import: boolean | "invite" | "map";
email: string;
}[];
};
config: {
sync: boolean;
};
project_id: string;
}
export interface IGithubRepoCollaborator {
avatar_url: string;
html_url: string;
id: number;
login: string;
url: string;
}
export interface IGithubRepoInfo {
issue_count: number;
labels: number;
collaborators: IGithubRepoCollaborator[];
}

View File

@ -0,0 +1,33 @@
export * from "./github-importer";
import { IProjectLite } from "types/projects";
// types
import { IUserLite } from "types/users";
export interface IImporterService {
created_at: string;
config: {
sync: boolean;
};
created_by: string | null;
data: {
users: [];
};
id: string;
initiated_by: string;
initiated_by_detail: IUserLite;
metadata: {
name: string;
owner: string;
repository_id: number;
url: string;
};
project: string;
project_detail: IProjectLite;
service: string;
status: "processing" | "completed" | "failed";
updated_at: string;
updated_by: string;
token: string;
workspace: string;
}

View File

@ -10,6 +10,7 @@ export * from "./views";
export * from "./integration";
export * from "./pages";
export * from "./ai";
export * from "./importer";
export type NestedKeyOf<ObjectType extends object> = {
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object