mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
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:
parent
6b8b981e1d
commit
c9d8a8dbd1
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
</>
|
||||
);
|
66
apps/app/components/integration/github/import-configure.tsx
Normal file
66
apps/app/components/integration/github/import-configure.tsx
Normal 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>
|
||||
);
|
||||
};
|
27
apps/app/components/integration/github/import-confirm.tsx
Normal file
27
apps/app/components/integration/github/import-confirm.tsx
Normal 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>
|
||||
);
|
@ -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">
|
||||
<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")}
|
||||
>
|
||||
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-issues")}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
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={`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)}
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
</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
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
59
apps/app/components/integration/github/import-users.tsx
Normal file
59
apps/app/components/integration/github/import-users.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
105
apps/app/components/integration/github/repo-details.tsx
Normal file
105
apps/app/components/integration/github/repo-details.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -1,168 +1,271 @@
|
||||
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 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",
|
||||
icon: CogIcon,
|
||||
},
|
||||
{
|
||||
title: "Import Data",
|
||||
key: "import-data",
|
||||
icon: CloudUploadIcon,
|
||||
},
|
||||
{ title: "Issues", key: "repo-details", icon: ListBulletIcon },
|
||||
{
|
||||
title: "Users",
|
||||
key: "import-users",
|
||||
icon: UsersIcon,
|
||||
},
|
||||
{
|
||||
title: "Confirm",
|
||||
key: "import-confirm",
|
||||
icon: CheckIcon,
|
||||
},
|
||||
];
|
||||
|
||||
export const GithubIntegrationRoot: FC<Props> = ({
|
||||
workspaceSlug,
|
||||
provider,
|
||||
allIntegrations,
|
||||
allIntegrationsError,
|
||||
allWorkspaceIntegrations,
|
||||
allWorkspaceIntegrationsError,
|
||||
allIntegrationImporters,
|
||||
allIntegrationImportersError,
|
||||
appIntegrations,
|
||||
workspaceIntegrations,
|
||||
}) => {
|
||||
const integrationWorkflowData = [
|
||||
{
|
||||
title: "Configure",
|
||||
key: "import-configure",
|
||||
icon: CogIcon,
|
||||
},
|
||||
{
|
||||
title: "Import Data",
|
||||
key: "import-import-data",
|
||||
icon: CloudUploadIcon,
|
||||
},
|
||||
{ title: "Issues", key: "migrate-issues", icon: UsersIcon },
|
||||
{
|
||||
title: "Users",
|
||||
key: "migrate-users",
|
||||
icon: ImportLayersIcon,
|
||||
},
|
||||
{
|
||||
title: "Confirm",
|
||||
key: "migrate-confirm",
|
||||
icon: CheckIcon,
|
||||
},
|
||||
];
|
||||
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">
|
||||
<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" />
|
||||
<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>Cancel import & go back</div>
|
||||
</div>
|
||||
<div>Back</div>
|
||||
</div>
|
||||
</Link>
|
||||
</Link>
|
||||
|
||||
<div className="space-y-4 rounded 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) => (
|
||||
<>
|
||||
<div
|
||||
key={_integration?.key}
|
||||
className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full border
|
||||
${
|
||||
_idx <= activeIntegrationState()
|
||||
? `border-[#3F76FF] bg-[#3F76FF] text-white ${
|
||||
_idx === activeIntegrationState()
|
||||
? `border-opacity-100 bg-opacity-100`
|
||||
: `border-opacity-80 bg-opacity-80`
|
||||
}`
|
||||
: `border-gray-300`
|
||||
}
|
||||
`}
|
||||
>
|
||||
<_integration.icon
|
||||
width={`18px`}
|
||||
height={`18px`}
|
||||
color={_idx <= activeIntegrationState() ? "#ffffff" : "#d1d5db"}
|
||||
/>
|
||||
</div>
|
||||
{_idx < integrationWorkflowData.length - 1 && (
|
||||
<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, index) => (
|
||||
<>
|
||||
<div
|
||||
key={_idx}
|
||||
className={`border-b px-7 ${
|
||||
_idx <= activeIntegrationState() - 1 ? `border-[#3F76FF]` : `border-gray-300`
|
||||
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 ${
|
||||
index === activeIntegrationState()
|
||||
? "border-opacity-100 bg-opacity-100"
|
||||
: "border-opacity-80 bg-opacity-80"
|
||||
}`
|
||||
: "border-gray-300"
|
||||
}`}
|
||||
>
|
||||
{" "}
|
||||
<integration.icon
|
||||
width="18px"
|
||||
height="18px"
|
||||
color={index <= activeIntegrationState() ? "#ffffff" : "#d1d5db"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
{index < integrationWorkflowData.length - 1 && (
|
||||
<div
|
||||
key={index}
|
||||
className={`border-b px-7 ${
|
||||
index <= activeIntegrationState() - 1
|
||||
? `border-[#3F76FF]`
|
||||
: `border-gray-300`
|
||||
}`}
|
||||
>
|
||||
{" "}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative w-full space-y-4 overflow-hidden">
|
||||
<div className="w-full">
|
||||
{integrationData?.state === "import-configure" && (
|
||||
<GithubConfigure
|
||||
state={integrationData}
|
||||
handleState={handleIntegrationData}
|
||||
workspaceSlug={workspaceSlug}
|
||||
provider={provider}
|
||||
allIntegrations={allIntegrations}
|
||||
allIntegrationsError={allIntegrationsError}
|
||||
allWorkspaceIntegrations={allWorkspaceIntegrations}
|
||||
allWorkspaceIntegrationsError={allWorkspaceIntegrationsError}
|
||||
/>
|
||||
)}
|
||||
{integrationData?.state === "import-import-data" && (
|
||||
<GithubImportData state={integrationData} handleState={handleIntegrationData} />
|
||||
)}
|
||||
{integrationData?.state === "migrate-issues" && (
|
||||
<GithubIssuesSelect state={integrationData} handleState={handleIntegrationData} />
|
||||
)}
|
||||
{integrationData?.state === "migrate-users" && (
|
||||
<GithubUsersSelect state={integrationData} handleState={handleIntegrationData} />
|
||||
)}
|
||||
{integrationData?.state === "migrate-confirm" && (
|
||||
<GithubConfirm state={integrationData} handleState={handleIntegrationData} />
|
||||
)}
|
||||
<div className="relative w-full space-y-4">
|
||||
<div className="w-full">
|
||||
{currentStep?.state === "import-configure" && (
|
||||
<GithubImportConfigure
|
||||
handleStepChange={handleStepChange}
|
||||
provider={provider}
|
||||
appIntegrations={appIntegrations}
|
||||
workspaceIntegrations={workspaceIntegrations}
|
||||
/>
|
||||
)}
|
||||
{currentStep?.state === "import-data" && (
|
||||
<GithubImportData
|
||||
handleStepChange={handleStepChange}
|
||||
integration={workspaceIntegration}
|
||||
control={control}
|
||||
watch={watch}
|
||||
/>
|
||||
)}
|
||||
{currentStep?.state === "repo-details" && (
|
||||
<GithubRepoDetails
|
||||
selectedRepo={watch("github")}
|
||||
handleStepChange={handleStepChange}
|
||||
setUsers={setUsers}
|
||||
setValue={setValue}
|
||||
/>
|
||||
)}
|
||||
{currentStep?.state === "import-users" && (
|
||||
<GithubImportUsers
|
||||
handleStepChange={handleStepChange}
|
||||
users={users}
|
||||
setUsers={setUsers}
|
||||
watch={watch}
|
||||
/>
|
||||
)}
|
||||
{currentStep?.state === "import-confirm" && (
|
||||
<GithubImportConfirm handleStepChange={handleStepChange} watch={watch} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
95
apps/app/components/integration/github/select-repository.tsx
Normal file
95
apps/app/components/integration/github/select-repository.tsx
Normal 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"
|
||||
/>
|
||||
);
|
||||
};
|
133
apps/app/components/integration/github/single-user-select.tsx
Normal file
133
apps/app/components/integration/github/single-user-select.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
@ -1,162 +1,198 @@
|
||||
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,
|
||||
}) => (
|
||||
<div className="space-y-5">
|
||||
{!provider && (
|
||||
<>
|
||||
<div className="text-2xl font-semibold">Import</div>
|
||||
appIntegrations,
|
||||
workspaceIntegrations,
|
||||
importerServices,
|
||||
}) => {
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
<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 you’ve created in other tracking services.
|
||||
This tool will guide you to relocate the issue to Plane.
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{!provider && (
|
||||
<>
|
||||
<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 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">
|
||||
<div>Read More</div>
|
||||
<div>
|
||||
<ArrowRightIcon width={"18px"} color={"#3F76FF"} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 cursor-pointer items-center gap-2 text-sm font-medium text-[#3F76FF] hover:text-opacity-80">
|
||||
<div>Read More</div>
|
||||
<div>
|
||||
<ArrowRightIcon width={"18px"} color={"#3F76FF"} />
|
||||
</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"
|
||||
>
|
||||
<div className="flex items-center gap-4 whitespace-nowrap">
|
||||
<div className="h-[40px] w-[40px] flex-shrink-0">
|
||||
{_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>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
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>
|
||||
</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 ? (
|
||||
<></>
|
||||
) : (
|
||||
<div className="py-2 text-sm text-gray-800">
|
||||
Previous Imports not available.
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
<Loader className="grid grid-cols-1 gap-3">
|
||||
{["", ""].map((_integration, _idx) => (
|
||||
<Loader.Item height="40px" width="100%" />
|
||||
))}
|
||||
</Loader>
|
||||
</div>
|
||||
<div>
|
||||
{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" && (
|
||||
<Image src={GithubLogo} alt="GithubLogo" />
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full space-y-1">
|
||||
<div className="flex items-center gap-2 font-medium">
|
||||
<h3>{integration?.title}</h3>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
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`}>
|
||||
<PrimaryButton>Integrate Now</PrimaryButton>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
|
||||
<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">
|
||||
No previous imports available.
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<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 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.Item height="34px" width="100%" />
|
||||
</Loader>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{provider && (
|
||||
<>
|
||||
{provider === "github" && (
|
||||
<GithubIntegrationRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
provider={provider}
|
||||
allIntegrations={allIntegrations}
|
||||
allIntegrationsError={allIntegrationsError}
|
||||
allWorkspaceIntegrations={allWorkspaceIntegrations}
|
||||
allWorkspaceIntegrationsError={allWorkspaceIntegrationsError}
|
||||
allIntegrationImporters={allIntegrationImporters}
|
||||
allIntegrationImportersError={allIntegrationImportersError}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
{provider && provider === "github" && (
|
||||
<GithubIntegrationRoot
|
||||
provider={provider}
|
||||
appIntegrations={appIntegrations}
|
||||
workspaceIntegrations={workspaceIntegrations}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IntegrationGuide;
|
||||
|
@ -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";
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
? `${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`
|
||||
}
|
||||
>
|
||||
{({ 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>
|
||||
label={
|
||||
syncedGithubRepository && syncedGithubRepository.length > 0
|
||||
? `${syncedGithubRepository[0].repo_detail.owner}/${syncedGithubRepository[0].repo_detail.name}`
|
||||
: "Select Repository"
|
||||
}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
@ -134,15 +134,21 @@ export const CustomSearchSelect = ({
|
||||
{({ active, selected }) => (
|
||||
<>
|
||||
{option.content}
|
||||
<div
|
||||
className={`flex items-center justify-center rounded border border-gray-500 p-0.5 ${
|
||||
active || selected ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
>
|
||||
{multiple ? (
|
||||
<div
|
||||
className={`flex items-center justify-center rounded border border-gray-500 p-0.5 ${
|
||||
active || selected ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
>
|
||||
<CheckIcon
|
||||
className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<CheckIcon
|
||||
className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
|
@ -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) =>
|
||||
|
@ -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<{
|
||||
|
@ -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",
|
||||
|
@ -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 (
|
||||
|
@ -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,76 +14,55 @@ 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={
|
||||
<Breadcrumbs>
|
||||
<BreadcrumbItem title={`${workspaceSlug ?? "Workspace"}`} link={`/${workspaceSlug}`} />
|
||||
<BreadcrumbItem title="Members Settings" />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
settingsLayout
|
||||
>
|
||||
<section className="space-y-5">
|
||||
<IntegrationGuide
|
||||
workspaceSlug={workspaceSlug}
|
||||
provider={provider}
|
||||
allIntegrations={allIntegrations}
|
||||
allIntegrationsError={allIntegrationsError}
|
||||
allWorkspaceIntegrations={allWorkspaceIntegrations}
|
||||
allWorkspaceIntegrationsError={allWorkspaceIntegrationsError}
|
||||
allIntegrationImporters={allIntegrationImporters}
|
||||
allIntegrationImportersError={allIntegrationImportersError}
|
||||
/>
|
||||
</section>
|
||||
</AppLayout>
|
||||
</>
|
||||
<AppLayout
|
||||
memberType={props}
|
||||
breadcrumbs={
|
||||
<Breadcrumbs>
|
||||
<BreadcrumbItem title={`${workspaceSlug ?? "Workspace"}`} link={`/${workspaceSlug}`} />
|
||||
<BreadcrumbItem title="Members Settings" />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
settingsLayout
|
||||
>
|
||||
<IntegrationGuide
|
||||
provider={provider as string}
|
||||
appIntegrations={appIntegrations}
|
||||
workspaceIntegrations={workspaceIntegrations}
|
||||
importerServices={importerServices}
|
||||
/>
|
||||
</AppLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
33
apps/app/types/importer/github-importer.d.ts
vendored
Normal file
33
apps/app/types/importer/github-importer.d.ts
vendored
Normal 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[];
|
||||
}
|
33
apps/app/types/importer/index.ts
Normal file
33
apps/app/types/importer/index.ts
Normal 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;
|
||||
}
|
1
apps/app/types/index.d.ts
vendored
1
apps/app/types/index.d.ts
vendored
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user