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:
47
apps/web/core/components/integration/jira/confirm-import.tsx
Normal file
47
apps/web/core/components/integration/jira/confirm-import.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from "react";
|
||||
|
||||
// react hook form
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import type { IJiraImporterForm } from "@plane/types";
|
||||
|
||||
// types
|
||||
|
||||
export const JiraConfirmImport: React.FC = () => {
|
||||
const { watch } = useFormContext<IJiraImporterForm>();
|
||||
|
||||
return (
|
||||
<div className="h-full w-full overflow-y-auto">
|
||||
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||
<div className="col-span-2">
|
||||
<h3 className="text-lg font-semibold">Confirm</h3>
|
||||
</div>
|
||||
|
||||
<div className="col-span-1">
|
||||
<p className="text-sm text-custom-text-200">Migrating</p>
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="mb-2 text-lg font-semibold">{watch("data.total_issues")}</h4>
|
||||
<p className="text-sm text-custom-text-200">Work items</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="mb-2 text-lg font-semibold">{watch("data.total_states")}</h4>
|
||||
<p className="text-sm text-custom-text-200">States</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="mb-2 text-lg font-semibold">{watch("data.total_modules")}</h4>
|
||||
<p className="text-sm text-custom-text-200">Modules</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="mb-2 text-lg font-semibold">{watch("data.total_labels")}</h4>
|
||||
<p className="text-sm text-custom-text-200">Labels</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="mb-2 text-lg font-semibold">{watch("data.users").filter((user) => user.import).length}</h4>
|
||||
<p className="text-sm text-custom-text-200">User</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
223
apps/web/core/components/integration/jira/give-details.tsx
Normal file
223
apps/web/core/components/integration/jira/give-details.tsx
Normal file
@@ -0,0 +1,223 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useFormContext, Controller } from "react-hook-form";
|
||||
import { Plus } from "lucide-react";
|
||||
import { PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import type { IJiraImporterForm } from "@plane/types";
|
||||
// hooks
|
||||
// components
|
||||
import { CustomSelect, Input } from "@plane/ui";
|
||||
// helpers
|
||||
import { checkEmailValidity } from "@plane/utils";
|
||||
import { captureClick } from "@/helpers/event-tracker.helper";
|
||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
// types
|
||||
|
||||
export const JiraGetImportDetail: React.FC = observer(() => {
|
||||
// store hooks
|
||||
const { toggleCreateProjectModal } = useCommandPalette();
|
||||
const { workspaceProjectIds, getProjectById } = useProject();
|
||||
// form info
|
||||
const {
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useFormContext<IJiraImporterForm>();
|
||||
|
||||
return (
|
||||
<div className="h-full w-full space-y-8 overflow-y-auto">
|
||||
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||
<div className="col-span-1">
|
||||
<h3 className="font-semibold">Jira Personal Access Token</h3>
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Get to know your access token by navigating to{" "}
|
||||
<Link href="https://id.atlassian.com/manage-profile/security/api-tokens" target="_blank" rel="noreferrer">
|
||||
<span className="text-custom-primary underline">Atlassian Settings</span>
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="col-span-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="metadata.api_token"
|
||||
rules={{
|
||||
required: "Please enter your personal access token.",
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="metadata.api_token"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.metadata?.api_token)}
|
||||
placeholder="XXXXXXXX"
|
||||
className="w-full"
|
||||
autoComplete="off"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.metadata?.api_token && <p className="text-xs text-red-500">{errors.metadata.api_token.message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||
<div className="col-span-1">
|
||||
<h3 className="font-semibold">Jira Project Key</h3>
|
||||
<p className="text-sm text-custom-text-200">If XXX-123 is your work item, then enter XXX</p>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="metadata.project_key"
|
||||
rules={{
|
||||
required: "Please enter your project key.",
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="metadata.project_key"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.metadata?.project_key)}
|
||||
placeholder="LIN"
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.metadata?.project_key && (
|
||||
<p className="text-xs text-red-500">{errors.metadata.project_key.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||
<div className="col-span-1">
|
||||
<h3 className="font-semibold">Jira Email Address</h3>
|
||||
<p className="text-sm text-custom-text-200">Enter the Email account that you use in Jira account</p>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="metadata.email"
|
||||
rules={{
|
||||
required: "Please enter email address.",
|
||||
validate: (value) => checkEmailValidity(value) || "Please enter a valid email address",
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="metadata.email"
|
||||
type="email"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.metadata?.email)}
|
||||
placeholder="name@company.com"
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.metadata?.email && <p className="text-xs text-red-500">{errors.metadata.email.message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||
<div className="col-span-1">
|
||||
<h3 className="font-semibold">Jira Installation or Cloud Host Name</h3>
|
||||
<p className="text-sm text-custom-text-200">Enter your companies cloud host name</p>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="metadata.cloud_hostname"
|
||||
rules={{
|
||||
required: "Please enter your cloud host name.",
|
||||
validate: (value) => !/^https?:\/\//.test(value) || "Hostname should not begin with http:// or https://",
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="metadata.cloud_hostname"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.metadata?.cloud_hostname)}
|
||||
placeholder="my-company.atlassian.net"
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.metadata?.cloud_hostname && (
|
||||
<p className="text-xs text-red-500">{errors.metadata.cloud_hostname.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||
<div className="col-span-1">
|
||||
<h3 className="font-semibold">Import to project</h3>
|
||||
<p className="text-sm text-custom-text-200">Select which project you want to import to.</p>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="project_id"
|
||||
rules={{ required: "Please select a project." }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
input
|
||||
onChange={onChange}
|
||||
label={
|
||||
<span>
|
||||
{value && value.trim() !== "" ? (
|
||||
getProjectById(value)?.name
|
||||
) : (
|
||||
<span className="text-custom-text-200">Select a project</span>
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{workspaceProjectIds && workspaceProjectIds.length > 0 ? (
|
||||
workspaceProjectIds.map((projectId) => {
|
||||
const projectDetails = getProjectById(projectId);
|
||||
|
||||
if (!projectDetails) return;
|
||||
|
||||
return (
|
||||
<CustomSelect.Option key={projectId} value={projectId}>
|
||||
{projectDetails.name}
|
||||
</CustomSelect.Option>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="flex cursor-pointer select-none items-center space-x-2 truncate rounded px-1 py-1.5 text-custom-text-200">
|
||||
<p>You don{"'"}t have any project. Please create a project first.</p>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
data-ph-element={PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON}
|
||||
onClick={() => {
|
||||
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.CREATE_PROJECT_JIRA_IMPORT_DETAIL_PAGE });
|
||||
toggleCreateProjectModal(true);
|
||||
}}
|
||||
className="flex cursor-pointer select-none items-center space-x-2 truncate rounded px-1 py-1.5 text-custom-text-200"
|
||||
>
|
||||
<Plus className="h-4 w-4 text-custom-text-200" />
|
||||
<span>Create new project</span>
|
||||
</button>
|
||||
</div>
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
154
apps/web/core/components/integration/jira/import-users.tsx
Normal file
154
apps/web/core/components/integration/jira/import-users.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useFormContext, useFieldArray, Controller } from "react-hook-form";
|
||||
import useSWR from "swr";
|
||||
// plane types
|
||||
import type { IJiraImporterForm } from "@plane/types";
|
||||
// plane ui
|
||||
import { Avatar, CustomSelect, CustomSearchSelect, Input, ToggleSwitch } 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";
|
||||
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
export const JiraImportUsers: FC = () => {
|
||||
const { workspaceSlug } = useParams();
|
||||
// form info
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useFormContext<IJiraImporterForm>();
|
||||
|
||||
const { fields } = useFieldArray({
|
||||
control,
|
||||
name: "data.users",
|
||||
});
|
||||
|
||||
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.email,
|
||||
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="h-full w-full space-y-10 divide-y-2 divide-custom-border-200 overflow-y-auto">
|
||||
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||
<div className="col-span-1">
|
||||
<h3 className="font-semibold">Users</h3>
|
||||
<p className="text-sm text-custom-text-200">Update, invite or choose not to invite assignee</p>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="data.invite_users"
|
||||
render={({ field: { value, onChange } }) => <ToggleSwitch onChange={onChange} value={value} />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{watch("data.invite_users") && (
|
||||
<div className="pt-6">
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="col-span-1 text-sm text-custom-text-200">Name</div>
|
||||
<div className="col-span-1 text-sm text-custom-text-200">Import as</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 space-y-3">
|
||||
{fields.map((user, index) => (
|
||||
<div className="grid grid-cols-3 gap-3" key={`${user.email}-${user.username}`}>
|
||||
<div className="col-span-1">
|
||||
<p>{user.username}</p>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`data.users.${index}.import`}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomSelect
|
||||
input
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
label={<span className="capitalize">{Boolean(value) ? value : ("Ignore" as any)}</span>}
|
||||
>
|
||||
<CustomSelect.Option value="invite">Invite by email</CustomSelect.Option>
|
||||
<CustomSelect.Option value="map">Map to existing</CustomSelect.Option>
|
||||
<CustomSelect.Option value={false}>Do not import</CustomSelect.Option>
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
{watch(`data.users.${index}.import`) === "invite" && (
|
||||
<Controller
|
||||
control={control}
|
||||
name={`data.users.${index}.email`}
|
||||
rules={{
|
||||
required: "This field is required",
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id={`data.users.${index}.email`}
|
||||
name={`data.users.${index}.email`}
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.data?.users?.[index]?.email)}
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{watch(`data.users.${index}.import`) === "map" && (
|
||||
<Controller
|
||||
control={control}
|
||||
name={`data.users.${index}.email`}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomSearchSelect
|
||||
value={value}
|
||||
input
|
||||
label={value !== "" ? value : "Select user from project"}
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
optionsClassName="w-48"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
39
apps/web/core/components/integration/jira/index.ts
Normal file
39
apps/web/core/components/integration/jira/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export * from "./root";
|
||||
export * from "./give-details";
|
||||
export * from "./jira-project-detail";
|
||||
export * from "./import-users";
|
||||
export * from "./confirm-import";
|
||||
|
||||
import type { IJiraImporterForm } from "@plane/types";
|
||||
|
||||
export type TJiraIntegrationSteps =
|
||||
| "import-configure"
|
||||
| "display-import-data"
|
||||
| "select-import-data"
|
||||
| "import-users"
|
||||
| "import-confirmation";
|
||||
|
||||
export interface IJiraIntegrationData {
|
||||
state: TJiraIntegrationSteps;
|
||||
}
|
||||
|
||||
export const jiraFormDefaultValues: IJiraImporterForm = {
|
||||
metadata: {
|
||||
cloud_hostname: "",
|
||||
api_token: "",
|
||||
project_key: "",
|
||||
email: "",
|
||||
},
|
||||
config: {
|
||||
epics_to_modules: false,
|
||||
},
|
||||
data: {
|
||||
users: [],
|
||||
invite_users: true,
|
||||
total_issues: 0,
|
||||
total_labels: 0,
|
||||
total_modules: 0,
|
||||
total_states: 0,
|
||||
},
|
||||
project_id: "",
|
||||
};
|
||||
@@ -0,0 +1,169 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
// next
|
||||
import { useParams } from "next/navigation";
|
||||
|
||||
// swr
|
||||
import { useFormContext, Controller } from "react-hook-form";
|
||||
import useSWR from "swr";
|
||||
import type { IJiraImporterForm, IJiraMetadata } from "@plane/types";
|
||||
|
||||
// react hook form
|
||||
|
||||
// services
|
||||
import { ToggleSwitch, Spinner } from "@plane/ui";
|
||||
import { JIRA_IMPORTER_DETAIL } from "@/constants/fetch-keys";
|
||||
import { JiraImporterService } from "@/services/integrations";
|
||||
|
||||
// fetch keys
|
||||
|
||||
// components
|
||||
|
||||
import type { IJiraIntegrationData, TJiraIntegrationSteps } from ".";
|
||||
|
||||
type Props = {
|
||||
setCurrentStep: React.Dispatch<React.SetStateAction<IJiraIntegrationData>>;
|
||||
setDisableTopBarAfter: React.Dispatch<React.SetStateAction<TJiraIntegrationSteps | null>>;
|
||||
};
|
||||
|
||||
// services
|
||||
const jiraImporterService = new JiraImporterService();
|
||||
|
||||
export const JiraProjectDetail: React.FC<Props> = (props) => {
|
||||
const { setCurrentStep, setDisableTopBarAfter } = props;
|
||||
|
||||
const {
|
||||
watch,
|
||||
setValue,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useFormContext<IJiraImporterForm>();
|
||||
|
||||
const { workspaceSlug } = useParams();
|
||||
|
||||
const params: IJiraMetadata = {
|
||||
api_token: watch("metadata.api_token"),
|
||||
project_key: watch("metadata.project_key"),
|
||||
email: watch("metadata.email"),
|
||||
cloud_hostname: watch("metadata.cloud_hostname"),
|
||||
};
|
||||
|
||||
const { data: projectInfo, error } = useSWR(
|
||||
workspaceSlug &&
|
||||
!errors.metadata?.api_token &&
|
||||
!errors.metadata?.project_key &&
|
||||
!errors.metadata?.email &&
|
||||
!errors.metadata?.cloud_hostname
|
||||
? JIRA_IMPORTER_DETAIL(workspaceSlug.toString(), params)
|
||||
: null,
|
||||
workspaceSlug &&
|
||||
!errors.metadata?.api_token &&
|
||||
!errors.metadata?.project_key &&
|
||||
!errors.metadata?.email &&
|
||||
!errors.metadata?.cloud_hostname
|
||||
? () => jiraImporterService.getJiraProjectInfo(workspaceSlug.toString(), params)
|
||||
: null
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectInfo) return;
|
||||
|
||||
setValue("data.total_issues", projectInfo.issues);
|
||||
setValue("data.total_labels", projectInfo.labels);
|
||||
setValue(
|
||||
"data.users",
|
||||
projectInfo.users?.map((user) => ({
|
||||
email: user.emailAddress,
|
||||
import: false,
|
||||
username: user.displayName,
|
||||
}))
|
||||
);
|
||||
setValue("data.total_states", projectInfo.states);
|
||||
setValue("data.total_modules", projectInfo.modules);
|
||||
}, [projectInfo, setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) setDisableTopBarAfter("display-import-data");
|
||||
else setDisableTopBarAfter(null);
|
||||
}, [error, setDisableTopBarAfter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectInfo && !error) setDisableTopBarAfter("display-import-data");
|
||||
else if (!error) setDisableTopBarAfter(null);
|
||||
}, [projectInfo, error, setDisableTopBarAfter]);
|
||||
|
||||
if (!projectInfo && !error) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Something went wrong. Please{" "}
|
||||
<button
|
||||
onClick={() => setCurrentStep({ state: "import-configure" })}
|
||||
type="button"
|
||||
className="inline text-custom-primary underline"
|
||||
>
|
||||
go back
|
||||
</button>{" "}
|
||||
and check your Jira project details.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full w-full space-y-10 overflow-y-auto">
|
||||
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||
<div className="col-span-1">
|
||||
<h3 className="font-semibold">Import Data</h3>
|
||||
<p className="text-sm text-custom-text-200">Import Completed. We have found:</p>
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="mb-2 text-lg font-semibold">{projectInfo?.issues}</h4>
|
||||
<p className="text-sm text-custom-text-200">Work items</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="mb-2 text-lg font-semibold">{projectInfo?.states}</h4>
|
||||
<p className="text-sm text-custom-text-200">States</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="mb-2 text-lg font-semibold">{projectInfo?.modules}</h4>
|
||||
<p className="text-sm text-custom-text-200">Modules</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="mb-2 text-lg font-semibold">{projectInfo?.labels}</h4>
|
||||
<p className="text-sm text-custom-text-200">Labels</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="mb-2 text-lg font-semibold">{projectInfo?.users?.length}</h4>
|
||||
<p className="text-sm text-custom-text-200">Users</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||
<div className="col-span-1">
|
||||
<h3 className="font-semibold">Import Epics</h3>
|
||||
<p className="text-sm text-custom-text-200">Import epics as modules</p>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="config.epics_to_modules"
|
||||
render={({ field: { value, onChange } }) => <ToggleSwitch onChange={onChange} value={value} />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
203
apps/web/core/components/integration/jira/root.tsx
Normal file
203
apps/web/core/components/integration/jira/root.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { mutate } from "swr";
|
||||
// icons
|
||||
import { ArrowLeft, Check, List, Settings } from "lucide-react";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { MembersPropertyIcon } from "@plane/propel/icons";
|
||||
// types
|
||||
import type { IJiraImporterForm } from "@plane/types";
|
||||
// assets
|
||||
import JiraLogo from "@/app/assets/services/jira.svg?url";
|
||||
// fetch keys
|
||||
import { IMPORTER_SERVICES_LIST } from "@/constants/fetch-keys";
|
||||
// hooks
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// services
|
||||
import { JiraImporterService } from "@/services/integrations";
|
||||
// components
|
||||
import type { TJiraIntegrationSteps, IJiraIntegrationData } from ".";
|
||||
import { JiraGetImportDetail, JiraProjectDetail, JiraImportUsers, JiraConfirmImport, jiraFormDefaultValues } from ".";
|
||||
|
||||
const integrationWorkflowData: Array<{
|
||||
title: string;
|
||||
key: TJiraIntegrationSteps;
|
||||
icon: any;
|
||||
}> = [
|
||||
{
|
||||
title: "Configure",
|
||||
key: "import-configure",
|
||||
icon: Settings,
|
||||
},
|
||||
{
|
||||
title: "Import Data",
|
||||
key: "display-import-data",
|
||||
icon: List,
|
||||
},
|
||||
{
|
||||
title: "Users",
|
||||
key: "import-users",
|
||||
icon: MembersPropertyIcon,
|
||||
},
|
||||
{
|
||||
title: "Confirm",
|
||||
key: "import-confirmation",
|
||||
icon: Check,
|
||||
},
|
||||
];
|
||||
|
||||
// services
|
||||
const jiraImporterService = new JiraImporterService();
|
||||
|
||||
export const JiraImporterRoot: React.FC = () => {
|
||||
const [currentStep, setCurrentStep] = useState<IJiraIntegrationData>({
|
||||
state: "import-configure",
|
||||
});
|
||||
const [disableTopBarAfter, setDisableTopBarAfter] = useState<TJiraIntegrationSteps | null>(null);
|
||||
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug } = useParams();
|
||||
|
||||
const methods = useForm<IJiraImporterForm>({
|
||||
defaultValues: jiraFormDefaultValues,
|
||||
mode: "all",
|
||||
reValidateMode: "onChange",
|
||||
});
|
||||
|
||||
const isValid = methods.formState.isValid;
|
||||
|
||||
const onSubmit = async (data: IJiraImporterForm) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
await jiraImporterService
|
||||
.createJiraImporter(workspaceSlug.toString(), data)
|
||||
.then(() => {
|
||||
mutate(IMPORTER_SERVICES_LIST(workspaceSlug.toString()));
|
||||
router.push(`/${workspaceSlug}/settings/imports`);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
const activeIntegrationState = () => {
|
||||
const currentElementIndex = integrationWorkflowData.findIndex((i) => i?.key === currentStep?.state);
|
||||
|
||||
return currentElementIndex;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex h-full flex-col 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">
|
||||
<div>
|
||||
<ArrowLeft className="h-3 w-3" />
|
||||
</div>
|
||||
<div>Cancel import & go back</div>
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<div className="flex h-full flex-col 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={JiraLogo} alt="jira logo" />
|
||||
</div>
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
{integrationWorkflowData.map((integration, index) => (
|
||||
<React.Fragment key={integration.key}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setCurrentStep({ state: integration.key });
|
||||
}}
|
||||
disabled={
|
||||
index > activeIntegrationState() + 1 ||
|
||||
Boolean(index === activeIntegrationState() + 1 && disableTopBarAfter)
|
||||
}
|
||||
className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full border border-custom-border-200 ${
|
||||
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"}`}
|
||||
/>
|
||||
</button>
|
||||
{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 h-full w-full pt-6">
|
||||
<FormProvider {...methods}>
|
||||
<form className="flex h-full w-full flex-col">
|
||||
<div className="h-full w-full overflow-y-auto">
|
||||
{currentStep.state === "import-configure" && <JiraGetImportDetail />}
|
||||
{currentStep.state === "display-import-data" && (
|
||||
<JiraProjectDetail setDisableTopBarAfter={setDisableTopBarAfter} setCurrentStep={setCurrentStep} />
|
||||
)}
|
||||
{currentStep?.state === "import-users" && <JiraImportUsers />}
|
||||
{currentStep?.state === "import-confirmation" && <JiraConfirmImport />}
|
||||
</div>
|
||||
|
||||
<div className="-mx-4 mt-4 flex justify-end gap-4 border-t border-custom-border-200 p-4 pb-0">
|
||||
{currentStep?.state !== "import-configure" && (
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
onClick={() => {
|
||||
const currentElementIndex = integrationWorkflowData.findIndex(
|
||||
(i) => i?.key === currentStep?.state
|
||||
);
|
||||
setCurrentStep({
|
||||
state: integrationWorkflowData[currentElementIndex - 1]?.key,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={disableTopBarAfter === currentStep?.state || !isValid || methods.formState.isSubmitting}
|
||||
onClick={() => {
|
||||
const currentElementIndex = integrationWorkflowData.findIndex((i) => i?.key === currentStep?.state);
|
||||
|
||||
if (currentElementIndex === integrationWorkflowData.length - 1) {
|
||||
methods.handleSubmit(onSubmit)();
|
||||
} else {
|
||||
setCurrentStep({
|
||||
state: integrationWorkflowData[currentElementIndex + 1]?.key,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{currentStep?.state === "import-confirmation" ? "Confirm & Import" : "Next"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user