Initial commit: Plane
Some checks failed
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Admin Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Sync Repositories / sync_changes (push) Has been cancelled
Some checks failed
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Admin Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Sync Repositories / sync_changes (push) Has been cancelled
Synced from upstream: 8853637e981ed7d8a6cff32bd98e7afe20f54362
This commit is contained in:
40
apps/web/core/components/integration/github/auth.tsx
Normal file
40
apps/web/core/components/integration/github/auth.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// types
|
||||
import { Button } from "@plane/propel/button";
|
||||
import type { IWorkspaceIntegration } from "@plane/types";
|
||||
// ui
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store/use-instance";
|
||||
import useIntegrationPopup from "@/hooks/use-integration-popup";
|
||||
|
||||
type Props = {
|
||||
workspaceIntegration: false | IWorkspaceIntegration | undefined;
|
||||
provider: string | undefined;
|
||||
};
|
||||
|
||||
export const GithubAuth: React.FC<Props> = observer(({ workspaceIntegration, provider }) => {
|
||||
// store hooks
|
||||
const { config } = useInstance();
|
||||
// hooks
|
||||
const { startAuth, isConnecting } = useIntegrationPopup({
|
||||
provider,
|
||||
github_app_name: config?.github_app_name || "",
|
||||
slack_client_id: config?.slack_client_id || "",
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
{workspaceIntegration && workspaceIntegration?.id ? (
|
||||
<Button variant="primary" disabled>
|
||||
Successfully Connected
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="primary" onClick={startAuth} loading={isConnecting}>
|
||||
{isConnecting ? "Connecting..." : "Connect"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
// components
|
||||
import { Button } from "@plane/propel/button";
|
||||
import type { IAppIntegration, IWorkspaceIntegration } from "@plane/types";
|
||||
import type { TIntegrationSteps } from "@/components/integration";
|
||||
import { GithubAuth } from "@/components/integration";
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
provider: string | undefined;
|
||||
handleStepChange: (value: TIntegrationSteps) => void;
|
||||
appIntegrations: IAppIntegration[] | undefined;
|
||||
workspaceIntegrations: IWorkspaceIntegration[] | undefined;
|
||||
};
|
||||
|
||||
export const GithubImportConfigure: React.FC<Props> = ({
|
||||
handleStepChange,
|
||||
provider,
|
||||
appIntegrations,
|
||||
workspaceIntegrations,
|
||||
}) => {
|
||||
// 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-custom-text-200">Set up your GitHub import.</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<GithubAuth workspaceIntegration={workspaceIntegration} provider={provider} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => handleStepChange("import-data")}
|
||||
disabled={workspaceIntegration && workspaceIntegration?.id ? false : true}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
|
||||
// react-hook-form
|
||||
import type { UseFormWatch } from "react-hook-form";
|
||||
// ui
|
||||
import { Button } from "@plane/propel/button";
|
||||
// types
|
||||
import type { 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 text-custom-text-200">
|
||||
You are about to import work items from {watch("github").full_name}. Click on {'"'}Confirm & Import{'" '}
|
||||
to complete the process.
|
||||
</h4>
|
||||
<div className="mt-6 flex items-center justify-between">
|
||||
<Button variant="neutral-primary" onClick={() => handleStepChange("import-users")}>
|
||||
Back
|
||||
</Button>
|
||||
<Button variant="primary" type="submit">
|
||||
Confirm & Import
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
127
apps/web/core/components/integration/github/import-data.tsx
Normal file
127
apps/web/core/components/integration/github/import-data.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import type { Control, UseFormWatch } from "react-hook-form";
|
||||
import { Controller } from "react-hook-form";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import type { IWorkspaceIntegration } from "@plane/types";
|
||||
// hooks
|
||||
// components
|
||||
import { CustomSearchSelect, ToggleSwitch } from "@plane/ui";
|
||||
import { truncateText } from "@plane/utils";
|
||||
import type { TFormValues, TIntegrationSteps } from "@/components/integration";
|
||||
import { SelectRepository } from "@/components/integration";
|
||||
// ui
|
||||
// helpers
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
handleStepChange: (value: TIntegrationSteps) => void;
|
||||
integration: IWorkspaceIntegration | false | undefined;
|
||||
control: Control<TFormValues, any>;
|
||||
watch: UseFormWatch<TFormValues>;
|
||||
};
|
||||
|
||||
export const GithubImportData: FC<Props> = observer((props) => {
|
||||
const { handleStepChange, integration, control, watch } = props;
|
||||
// store hooks
|
||||
const { workspaceProjectIds, getProjectById } = useProject();
|
||||
|
||||
const options = workspaceProjectIds?.map((projectId) => {
|
||||
const projectDetails = getProjectById(projectId);
|
||||
|
||||
return {
|
||||
value: `${projectDetails?.id}`,
|
||||
query: `${projectDetails?.name}`,
|
||||
content: <p>{truncateText(projectDetails?.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-xs text-custom-text-200">
|
||||
Select the repository that you want the work items 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}` : <span className="text-custom-text-200">Select Repository</span>
|
||||
}
|
||||
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-xs text-custom-text-200">Select the project to import the work item to.</p>
|
||||
</div>
|
||||
<div className="col-span-12 sm:col-span-4">
|
||||
{workspaceProjectIds && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="project"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomSearchSelect
|
||||
value={value}
|
||||
label={
|
||||
value ? getProjectById(value)?.name : <span className="text-custom-text-200">Select Project</span>
|
||||
}
|
||||
onChange={onChange}
|
||||
options={options}
|
||||
optionsClassName="w-48"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</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 work item</h4>
|
||||
<p className="text-xs text-custom-text-200">Set whether you want to sync the work items or not.</p>
|
||||
</div>
|
||||
<div className="col-span-12 sm:col-span-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="sync"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<ToggleSwitch value={value} onChange={() => onChange(!value)} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 flex items-center justify-end gap-2">
|
||||
<Button variant="neutral-primary" onClick={() => handleStepChange("import-configure")}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => handleStepChange("repo-details")}
|
||||
disabled={!watch("github") || !watch("project")}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
53
apps/web/core/components/integration/github/import-users.tsx
Normal file
53
apps/web/core/components/integration/github/import-users.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
|
||||
// react-hook-form
|
||||
import type { UseFormWatch } from "react-hook-form";
|
||||
// ui
|
||||
import { Button } from "@plane/propel/button";
|
||||
// types
|
||||
import type { IUserDetails, TFormValues, TIntegrationSteps } from "@/components/integration";
|
||||
import { SingleUserSelect } 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 isInvalid = users.filter((u) => u.import !== false && u.email === "").length > 0;
|
||||
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<div>
|
||||
<div className="mb-2 grid grid-cols-3 gap-2 text-sm font-medium">
|
||||
<div className="text-custom-text-200">Name</div>
|
||||
<div className="text-custom-text-200">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">
|
||||
<Button variant="neutral-primary" onClick={() => handleStepChange("repo-details")}>
|
||||
Back
|
||||
</Button>
|
||||
<Button variant="primary" onClick={() => handleStepChange("import-confirm")} disabled={isInvalid}>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
9
apps/web/core/components/integration/github/index.ts
Normal file
9
apps/web/core/components/integration/github/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export * from "./auth";
|
||||
export * from "./import-configure";
|
||||
export * from "./import-confirm";
|
||||
export * from "./import-data";
|
||||
export * from "./import-users";
|
||||
export * from "./repo-details";
|
||||
export * from "./root";
|
||||
export * from "./select-repository";
|
||||
export * from "./single-user-select";
|
||||
106
apps/web/core/components/integration/github/repo-details.tsx
Normal file
106
apps/web/core/components/integration/github/repo-details.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
|
||||
// react-hook-form
|
||||
import type { UseFormSetValue } from "react-hook-form";
|
||||
import useSWR from "swr";
|
||||
// services
|
||||
// ui
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { Loader } from "@plane/ui";
|
||||
// types
|
||||
import type { IUserDetails, TFormValues, TIntegrationSteps } from "@/components/integration";
|
||||
// fetch-keys
|
||||
import { GITHUB_REPOSITORY_INFO } from "@/constants/fetch-keys";
|
||||
import { GithubIntegrationService } from "@/services/integrations";
|
||||
|
||||
type Props = {
|
||||
selectedRepo: any;
|
||||
handleStepChange: (value: TIntegrationSteps) => void;
|
||||
setUsers: React.Dispatch<React.SetStateAction<IUserDetails[]>>;
|
||||
setValue: UseFormSetValue<TFormValues>;
|
||||
};
|
||||
|
||||
// services
|
||||
const githubIntegrationService = new GithubIntegrationService();
|
||||
|
||||
export const GithubRepoDetails: FC<Props> = ({ selectedRepo, handleStepChange, setUsers, setValue }) => {
|
||||
const { workspaceSlug } = useParams();
|
||||
|
||||
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-custom-text-200">Import completed. We have found:</div>
|
||||
</div>
|
||||
<div className="mt-4 flex gap-16">
|
||||
<div className="flex-shrink-0 text-center">
|
||||
<p className="text-3xl font-bold">{repoInfo.issue_count}</p>
|
||||
<h6 className="text-sm text-custom-text-200">Work items</h6>
|
||||
</div>
|
||||
<div className="flex-shrink-0 text-center">
|
||||
<p className="text-3xl font-bold">{repoInfo.labels}</p>
|
||||
<h6 className="text-sm text-custom-text-200">Labels</h6>
|
||||
</div>
|
||||
<div className="flex-shrink-0 text-center">
|
||||
<p className="text-3xl font-bold">{repoInfo.collaborators.length}</p>
|
||||
<h6 className="text-sm text-custom-text-200">Users</h6>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<h5>We didn{"'"}t find any work item in this repository.</h5>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<Loader>
|
||||
<Loader.Item height="70px" />
|
||||
</Loader>
|
||||
)}
|
||||
<div className="mt-6 flex items-center justify-end gap-2">
|
||||
<Button variant="neutral-primary" onClick={() => handleStepChange("import-data")}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => handleStepChange("import-users")}
|
||||
disabled={!repoInfo || repoInfo.issue_count === 0}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
249
apps/web/core/components/integration/github/root.tsx
Normal file
249
apps/web/core/components/integration/github/root.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useParams, useSearchParams } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { ArrowLeft, Check, List, Settings, UploadCloud } from "lucide-react";
|
||||
import { MembersPropertyIcon } from "@plane/propel/icons";
|
||||
// types
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { IGithubRepoCollaborator, IGithubServiceImportFormData } from "@plane/types";
|
||||
// assets
|
||||
import GithubLogo from "@/app/assets/services/github.png?url";
|
||||
// components
|
||||
import {
|
||||
GithubImportConfigure,
|
||||
GithubImportData,
|
||||
GithubRepoDetails,
|
||||
GithubImportUsers,
|
||||
GithubImportConfirm,
|
||||
} from "@/components/integration";
|
||||
// fetch keys
|
||||
import { APP_INTEGRATIONS, IMPORTER_SERVICES_LIST, WORKSPACE_INTEGRATIONS } from "@/constants/fetch-keys";
|
||||
// hooks
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// services
|
||||
import { IntegrationService, GithubIntegrationService } from "@/services/integrations";
|
||||
|
||||
export type TIntegrationSteps = "import-configure" | "import-data" | "repo-details" | "import-users" | "import-confirm";
|
||||
export interface IIntegrationData {
|
||||
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: Settings,
|
||||
},
|
||||
{
|
||||
title: "Import Data",
|
||||
key: "import-data",
|
||||
icon: UploadCloud,
|
||||
},
|
||||
{ title: "Work item", key: "repo-details", icon: List },
|
||||
{
|
||||
title: "Users",
|
||||
key: "import-users",
|
||||
icon: MembersPropertyIcon,
|
||||
},
|
||||
{
|
||||
title: "Confirm",
|
||||
key: "import-confirm",
|
||||
icon: Check,
|
||||
},
|
||||
];
|
||||
|
||||
// services
|
||||
const integrationService = new IntegrationService();
|
||||
const githubIntegrationService = new GithubIntegrationService();
|
||||
|
||||
export const GithubImporterRoot: React.FC = () => {
|
||||
const [currentStep, setCurrentStep] = useState<IIntegrationData>({
|
||||
state: "import-configure",
|
||||
});
|
||||
const [users, setUsers] = useState<IUserDetails[]>([]);
|
||||
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug } = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const provider = searchParams.get("provider");
|
||||
|
||||
const { handleSubmit, control, setValue, watch } = useForm<TFormValues>({
|
||||
defaultValues: defaultFormValues,
|
||||
});
|
||||
|
||||
const { data: appIntegrations } = useSWR(APP_INTEGRATIONS, () => integrationService.getAppIntegrationsList());
|
||||
|
||||
const { data: workspaceIntegrations } = useSWR(
|
||||
workspaceSlug ? WORKSPACE_INTEGRATIONS(workspaceSlug as string) : null,
|
||||
workspaceSlug ? () => integrationService.getWorkspaceIntegrationsList(workspaceSlug as string) : null
|
||||
);
|
||||
|
||||
const activeIntegrationState = () => {
|
||||
const currentElementIndex = integrationWorkflowData.findIndex((i) => i?.key === currentStep?.state);
|
||||
|
||||
return currentElementIndex;
|
||||
};
|
||||
|
||||
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/imports`);
|
||||
mutate(IMPORTER_SERVICES_LIST(workspaceSlug as string));
|
||||
})
|
||||
.catch(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Import was unsuccessful. Please try again.",
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(createGithubImporterService)}>
|
||||
<div className="mt-4 space-y-2">
|
||||
<Link href={`/${workspaceSlug}/settings/imports`}>
|
||||
<span className="inline-flex cursor-pointer items-center gap-2 text-sm font-medium text-custom-text-200 hover:text-custom-text-100">
|
||||
<ArrowLeft className="h-3 w-3" />
|
||||
<div>Cancel import & go back</div>
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<div className="space-y-4 rounded-[10px] border border-custom-border-200 bg-custom-background-100 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) => (
|
||||
<React.Fragment key={integration.key}>
|
||||
<div
|
||||
className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full border ${
|
||||
index <= activeIntegrationState()
|
||||
? `border-custom-primary bg-custom-primary ${
|
||||
index === activeIntegrationState()
|
||||
? "border-opacity-100 bg-opacity-100"
|
||||
: "border-opacity-80 bg-opacity-80"
|
||||
}`
|
||||
: "border-custom-border-200"
|
||||
}`}
|
||||
>
|
||||
<integration.icon
|
||||
className={`h-5 w-5 ${index <= activeIntegrationState() ? "text-white" : "text-custom-text-400"}`}
|
||||
/>
|
||||
</div>
|
||||
{index < integrationWorkflowData.length - 1 && (
|
||||
<div
|
||||
key={index}
|
||||
className={`border-b px-7 ${
|
||||
index <= activeIntegrationState() - 1 ? `border-custom-primary` : `border-custom-border-200`
|
||||
}`}
|
||||
>
|
||||
{" "}
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative w-full space-y-4">
|
||||
<div className="w-full">
|
||||
{currentStep?.state === "import-configure" && (
|
||||
<GithubImportConfigure
|
||||
handleStepChange={handleStepChange}
|
||||
provider={provider as string}
|
||||
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>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWRInfinite from "swr/infinite";
|
||||
import type { IWorkspaceIntegration } from "@plane/types";
|
||||
// services
|
||||
// ui
|
||||
import { CustomSearchSelect } from "@plane/ui";
|
||||
// helpers
|
||||
import { truncateText } from "@plane/utils";
|
||||
import { ProjectService } from "@/services/project";
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
integration: IWorkspaceIntegration;
|
||||
value: any;
|
||||
label: string | React.ReactNode;
|
||||
onChange: (repo: any) => void;
|
||||
characterLimit?: number;
|
||||
};
|
||||
|
||||
const projectService = new ProjectService();
|
||||
|
||||
export const SelectRepository: React.FC<Props> = (props) => {
|
||||
const { integration, value, label, onChange, characterLimit = 25 } = props;
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
|
||||
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);
|
||||
|
||||
let userRepositories = (paginatedData ?? []).map((data) => data.repositories).flat();
|
||||
userRepositories = userRepositories.filter((data) => data?.id);
|
||||
|
||||
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>,
|
||||
})) ?? [];
|
||||
|
||||
if (userRepositories.length < 1) return null;
|
||||
|
||||
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-custom-text-200 hover:bg-custom-background-80"
|
||||
onClick={() => setSize(size + 1)}
|
||||
disabled={isValidating}
|
||||
>
|
||||
{isValidating ? "Loading..." : "Click to load more..."}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
optionsClassName="w-48"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,134 @@
|
||||
"use client";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// plane types
|
||||
import type { IGithubRepoCollaborator } from "@plane/types";
|
||||
// plane ui
|
||||
import { Avatar, CustomSelect, CustomSearchSelect, Input } from "@plane/ui";
|
||||
// constants
|
||||
import { getFileURL } from "@plane/utils";
|
||||
import { WORKSPACE_MEMBERS } from "@/constants/fetch-keys";
|
||||
// helpers
|
||||
// plane web services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// types
|
||||
import type { IUserDetails } from "./root";
|
||||
|
||||
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",
|
||||
},
|
||||
];
|
||||
|
||||
// services
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
export const SingleUserSelect: React.FC<Props> = ({ collaborator, index, users, setUsers }) => {
|
||||
const { workspaceSlug } = useParams();
|
||||
|
||||
const { data: members } = useSWR(
|
||||
workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug.toString()) : null,
|
||||
workspaceSlug ? () => workspaceService.fetchWorkspaceMembers(workspaceSlug.toString()) : null
|
||||
);
|
||||
|
||||
const options = members
|
||||
?.map((member) => {
|
||||
if (!member?.member) return;
|
||||
return {
|
||||
value: member.member?.display_name,
|
||||
query: member.member?.display_name ?? "",
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar name={member?.member?.display_name} src={getFileURL(member?.member?.avatar_url)} />
|
||||
{member.member?.display_name}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
})
|
||||
.filter((member) => !!member) as
|
||||
| {
|
||||
value: string;
|
||||
query: string;
|
||||
content: React.ReactNode;
|
||||
}[]
|
||||
| undefined;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 items-center gap-2 rounded-md bg-custom-background-80 px-2 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative h-8 w-8 flex-shrink-0 rounded">
|
||||
<img
|
||||
src={collaborator.avatar_url}
|
||||
className="absolute left-0 top-0 h-full w-full rounded object-cover"
|
||||
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);
|
||||
}}
|
||||
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
|
||||
id="email"
|
||||
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="w-full py-1 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-48"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user