forked from github/plane
feat: jira importer (#879)
* feat: jira importer * fix: yarn lock * fix: displaying correct count of users that are been imported * fix: showing workspace member in import users
This commit is contained in:
parent
2b280935a1
commit
63a36fb25d
@ -52,7 +52,7 @@ const IntegrationGuide = () => {
|
|||||||
handleClose={() => setDeleteImportModal(false)}
|
handleClose={() => setDeleteImportModal(false)}
|
||||||
data={importToDelete}
|
data={importToDelete}
|
||||||
/>
|
/>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 h-full">
|
||||||
{!provider && (
|
{!provider && (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-2 mb-5">
|
<div className="flex items-center gap-2 mb-5">
|
||||||
|
49
apps/app/components/integration/jira/confirm-import.tsx
Normal file
49
apps/app/components/integration/jira/confirm-import.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// react hook form
|
||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
// types
|
||||||
|
import { IJiraImporterForm } from "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-gray-500">Migrating</p>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h4 className="mb-2 text-xl font-semibold">{watch("data.total_issues")}</h4>
|
||||||
|
<p className="text-sm text-gray-500">Issues</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="mb-2 text-xl font-semibold">{watch("data.total_states")}</h4>
|
||||||
|
<p className="text-sm text-gray-500">States</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="mb-2 text-xl font-semibold">{watch("data.total_modules")}</h4>
|
||||||
|
<p className="text-sm text-gray-500">Modules</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="mb-2 text-xl font-semibold">{watch("data.total_labels")}</h4>
|
||||||
|
<p className="text-sm text-gray-500">Labels</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="mb-2 text-xl font-semibold">
|
||||||
|
{watch("data.users").filter((user) => user.import).length}
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-gray-500">User</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
178
apps/app/components/integration/jira/give-details.tsx
Normal file
178
apps/app/components/integration/jira/give-details.tsx
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// next
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
// react hook form
|
||||||
|
import { useFormContext, Controller } from "react-hook-form";
|
||||||
|
|
||||||
|
// icons
|
||||||
|
import { PlusIcon } from "@heroicons/react/20/solid";
|
||||||
|
|
||||||
|
// hooks
|
||||||
|
import useProjects from "hooks/use-projects";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { Input, CustomSelect } from "components/ui";
|
||||||
|
|
||||||
|
import { IJiraImporterForm } from "types";
|
||||||
|
|
||||||
|
export const JiraGetImportDetail: React.FC = () => {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
control,
|
||||||
|
formState: { errors },
|
||||||
|
} = useFormContext<IJiraImporterForm>();
|
||||||
|
|
||||||
|
const { projects } = useProjects();
|
||||||
|
|
||||||
|
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="text-lg font-semibold">Jira Personal Access Token</h3>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Get to know your access token by navigating to{" "}
|
||||||
|
<Link href="https://id.atlassian.com/manage-profile/security/api-tokens">
|
||||||
|
<a
|
||||||
|
className="font-medium text-gray-600 hover:text-gray-900"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Atlassian Settings
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-1">
|
||||||
|
<Input
|
||||||
|
id="metadata.api_token"
|
||||||
|
name="metadata.api_token"
|
||||||
|
placeholder="XXXXXXXX"
|
||||||
|
validations={{
|
||||||
|
required: "Please enter your personal access token.",
|
||||||
|
}}
|
||||||
|
register={register}
|
||||||
|
error={errors.metadata?.api_token}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||||
|
<div className="col-span-1">
|
||||||
|
<h3 className="text-lg font-semibold">Jira Project Key</h3>
|
||||||
|
<p className="text-sm text-gray-500">If XXX-123 is your issue, then enter XXX</p>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<Input
|
||||||
|
id="metadata.project_key"
|
||||||
|
name="metadata.project_key"
|
||||||
|
placeholder="LIN"
|
||||||
|
register={register}
|
||||||
|
validations={{
|
||||||
|
required: "Please enter your project key.",
|
||||||
|
}}
|
||||||
|
error={errors.metadata?.project_key}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||||
|
<div className="col-span-1">
|
||||||
|
<h3 className="text-lg font-semibold">Jira Email Address</h3>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Enter the Gmail account that you use in Jira account
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<Input
|
||||||
|
id="metadata.email"
|
||||||
|
name="metadata.email"
|
||||||
|
type="email"
|
||||||
|
placeholder="name@company.com"
|
||||||
|
register={register}
|
||||||
|
validations={{
|
||||||
|
required: "Please enter email address.",
|
||||||
|
}}
|
||||||
|
error={errors.metadata?.email}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||||
|
<div className="col-span-1">
|
||||||
|
<h3 className="text-lg font-semibold">Jira Installation or Cloud Host Name</h3>
|
||||||
|
<p className="text-sm text-gray-500">Enter your companies cloud host name</p>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<Input
|
||||||
|
id="metadata.cloud_hostname"
|
||||||
|
name="metadata.cloud_hostname"
|
||||||
|
type="email"
|
||||||
|
placeholder="my-company.atlassian.net"
|
||||||
|
register={register}
|
||||||
|
validations={{
|
||||||
|
required: "Please enter your cloud host name.",
|
||||||
|
}}
|
||||||
|
error={errors.metadata?.cloud_hostname}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||||
|
<div className="col-span-1">
|
||||||
|
<h3 className="text-lg font-semibold">Import to project</h3>
|
||||||
|
<p className="text-sm text-gray-500">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
|
||||||
|
width="w-full"
|
||||||
|
onChange={onChange}
|
||||||
|
label={
|
||||||
|
<span>
|
||||||
|
{value && value !== ""
|
||||||
|
? projects.find((p) => p.id === value)?.name
|
||||||
|
: "Select Project"}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{projects.length > 0 ? (
|
||||||
|
projects.map((project) => (
|
||||||
|
<CustomSelect.Option key={project.id} value={project.id}>
|
||||||
|
{project.name}
|
||||||
|
</CustomSelect.Option>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="flex cursor-pointer select-none items-center space-x-2 truncate rounded px-1 py-1.5 text-gray-500">
|
||||||
|
<p>You don{"'"}t have any project. Please create a project first.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
const event = new KeyboardEvent("keydown", { key: "p" });
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
}}
|
||||||
|
className="flex cursor-pointer select-none items-center space-x-2 truncate rounded px-1 py-1.5 text-gray-500"
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-4 w-4 text-gray-500" />
|
||||||
|
<span>Create new project</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</CustomSelect>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
145
apps/app/components/integration/jira/import-users.tsx
Normal file
145
apps/app/components/integration/jira/import-users.tsx
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
// next
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// react-hook-form
|
||||||
|
import { useFormContext, useFieldArray, Controller } from "react-hook-form";
|
||||||
|
|
||||||
|
// hooks
|
||||||
|
import useWorkspaceMembers from "hooks/use-workspace-members";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { ToggleSwitch, Input, CustomSelect, CustomSearchSelect, Avatar } from "components/ui";
|
||||||
|
|
||||||
|
import { IJiraImporterForm } from "types";
|
||||||
|
|
||||||
|
export const JiraImportUsers: FC = () => {
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
watch,
|
||||||
|
register,
|
||||||
|
formState: { errors },
|
||||||
|
} = useFormContext<IJiraImporterForm>();
|
||||||
|
|
||||||
|
const { fields } = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: "data.users",
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
const { workspaceMembers: members } = useWorkspaceMembers(workspaceSlug?.toString());
|
||||||
|
|
||||||
|
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="h-full w-full space-y-10 divide-y-2 overflow-y-auto">
|
||||||
|
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||||
|
<div className="col-span-1">
|
||||||
|
<h3 className="text-lg font-semibold">Users</h3>
|
||||||
|
<p className="text-sm text-gray-500">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-gray-500">Name</div>
|
||||||
|
<div className="col-span-1 text-gray-500">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}
|
||||||
|
width="w-full"
|
||||||
|
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" && (
|
||||||
|
<Input
|
||||||
|
id={`data.users.${index}.email`}
|
||||||
|
name={`data.users.${index}.email`}
|
||||||
|
type="text"
|
||||||
|
register={register}
|
||||||
|
validations={{
|
||||||
|
required: "This field is required",
|
||||||
|
}}
|
||||||
|
error={errors?.data?.users?.[index]?.email}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{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-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1 +1,39 @@
|
|||||||
export * from "./root";
|
export * from "./root";
|
||||||
|
export * from "./give-details";
|
||||||
|
export * from "./jira-project-detail";
|
||||||
|
export * from "./import-users";
|
||||||
|
export * from "./confirm-import";
|
||||||
|
|
||||||
|
import { IJiraImporterForm } from "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: "",
|
||||||
|
};
|
||||||
|
168
apps/app/components/integration/jira/jira-project-detail.tsx
Normal file
168
apps/app/components/integration/jira/jira-project-detail.tsx
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
|
// next
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// swr
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
// react hook form
|
||||||
|
import { useFormContext, Controller } from "react-hook-form";
|
||||||
|
|
||||||
|
// services
|
||||||
|
import jiraImporterService from "services/integration/jira.service";
|
||||||
|
|
||||||
|
// fetch keys
|
||||||
|
import { JIRA_IMPORTER_DETAIL } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
import { IJiraImporterForm, IJiraMetadata } from "types";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { Spinner, ToggleSwitch } from "components/ui";
|
||||||
|
|
||||||
|
import type { IJiraIntegrationData, TJiraIntegrationSteps } from "./";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
setCurrentStep: React.Dispatch<React.SetStateAction<IJiraIntegrationData>>;
|
||||||
|
setDisableTopBarAfter: React.Dispatch<React.SetStateAction<TJiraIntegrationSteps | null>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const JiraProjectDetail: React.FC<Props> = (props) => {
|
||||||
|
const { setCurrentStep, setDisableTopBarAfter } = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
|
control,
|
||||||
|
formState: { errors },
|
||||||
|
} = useFormContext<IJiraImporterForm>();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
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-gray-500">
|
||||||
|
Something went wrong. Please{" "}
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentStep({ state: "import-configure" })}
|
||||||
|
type="button"
|
||||||
|
className="inline text-blue-500 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="text-lg font-semibold">Import Data</h3>
|
||||||
|
<p className="text-sm text-gray-500">Import Completed. We have found:</p>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h4 className="mb-2 text-xl font-semibold">{projectInfo?.issues}</h4>
|
||||||
|
<p className="text-sm text-gray-500">Issues</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="mb-2 text-xl font-semibold">{projectInfo?.states}</h4>
|
||||||
|
<p className="text-sm text-gray-500">States</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="mb-2 text-xl font-semibold">{projectInfo?.modules}</h4>
|
||||||
|
<p className="text-sm text-gray-500">Modules</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="mb-2 text-xl font-semibold">{projectInfo?.labels}</h4>
|
||||||
|
<p className="text-sm text-gray-500">Labels</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="mb-2 text-xl font-semibold">{projectInfo?.users?.length}</h4>
|
||||||
|
<p className="text-sm text-gray-500">Users</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||||
|
<div className="col-span-1">
|
||||||
|
<h3 className="text-lg font-semibold">Import Epics</h3>
|
||||||
|
<p className="text-sm text-gray-500">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>
|
||||||
|
);
|
||||||
|
};
|
@ -1 +1,224 @@
|
|||||||
export const JiraImporterRoot = () => <></>;
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
// next
|
||||||
|
import Link from "next/link";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// swr
|
||||||
|
import { mutate } from "swr";
|
||||||
|
|
||||||
|
// react hook form
|
||||||
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
// icons
|
||||||
|
import { ArrowLeftIcon, ListBulletIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { CogIcon, CloudUploadIcon, UsersIcon, CheckIcon } from "components/icons";
|
||||||
|
|
||||||
|
// services
|
||||||
|
import jiraImporterService from "services/integration/jira.service";
|
||||||
|
|
||||||
|
// fetch keys
|
||||||
|
import { IMPORTER_SERVICES_LIST } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { PrimaryButton, SecondaryButton } from "components/ui";
|
||||||
|
import {
|
||||||
|
JiraGetImportDetail,
|
||||||
|
JiraProjectDetail,
|
||||||
|
JiraImportUsers,
|
||||||
|
JiraConfirmImport,
|
||||||
|
jiraFormDefaultValues,
|
||||||
|
TJiraIntegrationSteps,
|
||||||
|
IJiraIntegrationData,
|
||||||
|
} from "./";
|
||||||
|
|
||||||
|
import JiraLogo from "public/services/jira.png";
|
||||||
|
|
||||||
|
import { IJiraImporterForm } from "types";
|
||||||
|
|
||||||
|
const integrationWorkflowData: Array<{
|
||||||
|
title: string;
|
||||||
|
key: TJiraIntegrationSteps;
|
||||||
|
icon: React.FC<React.SVGProps<SVGSVGElement>>;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
title: "Configure",
|
||||||
|
key: "import-configure",
|
||||||
|
icon: CogIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Import Data",
|
||||||
|
key: "display-import-data",
|
||||||
|
icon: ListBulletIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Users",
|
||||||
|
key: "import-users",
|
||||||
|
icon: UsersIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Confirm",
|
||||||
|
key: "import-confirmation",
|
||||||
|
icon: CheckIcon,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const JiraImporterRoot = () => {
|
||||||
|
const [currentStep, setCurrentStep] = useState<IJiraIntegrationData>({
|
||||||
|
state: "import-configure",
|
||||||
|
});
|
||||||
|
const [disableTopBarAfter, setDisableTopBarAfter] = useState<TJiraIntegrationSteps | null>(null);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
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/import-export`);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeIntegrationState = () => {
|
||||||
|
const currentElementIndex = integrationWorkflowData.findIndex(
|
||||||
|
(i) => i?.key === currentStep?.state
|
||||||
|
);
|
||||||
|
|
||||||
|
return currentElementIndex;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full flex-col 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>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="flex h-full flex-col 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={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 ${
|
||||||
|
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"}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{index < integrationWorkflowData.length - 1 && (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`border-b px-7 ${
|
||||||
|
index <= activeIntegrationState() - 1 ? `border-[#3F76FF]` : `border-gray-300`
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
</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 p-4 pb-0">
|
||||||
|
{currentStep?.state !== "import-configure" && (
|
||||||
|
<SecondaryButton
|
||||||
|
onClick={() => {
|
||||||
|
const currentElementIndex = integrationWorkflowData.findIndex(
|
||||||
|
(i) => i?.key === currentStep?.state
|
||||||
|
);
|
||||||
|
setCurrentStep({
|
||||||
|
state: integrationWorkflowData[currentElementIndex - 1]?.key,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</SecondaryButton>
|
||||||
|
)}
|
||||||
|
<PrimaryButton
|
||||||
|
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"}
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -18,6 +18,7 @@ type CustomSearchSelectProps = {
|
|||||||
noChevron?: boolean;
|
noChevron?: boolean;
|
||||||
customButton?: JSX.Element;
|
customButton?: JSX.Element;
|
||||||
optionsClassName?: string;
|
optionsClassName?: string;
|
||||||
|
input?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
selfPositioned?: boolean;
|
selfPositioned?: boolean;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
@ -34,6 +35,7 @@ export const CustomSearchSelect = ({
|
|||||||
noChevron = false,
|
noChevron = false,
|
||||||
customButton,
|
customButton,
|
||||||
optionsClassName = "",
|
optionsClassName = "",
|
||||||
|
input = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
selfPositioned = false,
|
selfPositioned = false,
|
||||||
multiple = false,
|
multiple = false,
|
||||||
@ -68,7 +70,9 @@ export const CustomSearchSelect = ({
|
|||||||
<Combobox.Button
|
<Combobox.Button
|
||||||
className={`flex w-full ${
|
className={`flex w-full ${
|
||||||
disabled ? "cursor-not-allowed" : "cursor-pointer hover:bg-gray-100"
|
disabled ? "cursor-not-allowed" : "cursor-pointer hover:bg-gray-100"
|
||||||
} items-center justify-between gap-1 rounded-md border px-3 py-1.5 text-xs shadow-sm duration-300 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
|
} ${
|
||||||
|
input ? "border-gray-300 px-3 py-2 text-sm" : "px-2.5 py-1 text-xs"
|
||||||
|
} items-center justify-between gap-1 rounded-md border shadow-sm duration-300 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
|
||||||
textAlignment === "right"
|
textAlignment === "right"
|
||||||
? "text-right"
|
? "text-right"
|
||||||
: textAlignment === "center"
|
: textAlignment === "center"
|
||||||
|
@ -20,3 +20,4 @@ export * from "./labels-list";
|
|||||||
export * from "./linear-progress-indicator";
|
export * from "./linear-progress-indicator";
|
||||||
export * from "./empty-state";
|
export * from "./empty-state";
|
||||||
export * from "./multi-level-dropdown";
|
export * from "./multi-level-dropdown";
|
||||||
|
export * from "./toggle-switch";
|
||||||
|
32
apps/app/components/ui/toggle-switch.tsx
Normal file
32
apps/app/components/ui/toggle-switch.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Switch } from "@headlessui/react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value: boolean;
|
||||||
|
onChange: (value: boolean) => void;
|
||||||
|
label?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ToggleSwitch: React.FC<Props> = (props) => {
|
||||||
|
const { value, onChange, label, disabled, className } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
checked={value}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={onChange}
|
||||||
|
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"
|
||||||
|
} ${className || " "}`}
|
||||||
|
>
|
||||||
|
<span className="sr-only">{label}</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"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
};
|
@ -1,3 +1,5 @@
|
|||||||
|
import { IJiraMetadata } from "types";
|
||||||
|
|
||||||
const paramsToKey = (params: any) => {
|
const paramsToKey = (params: any) => {
|
||||||
const { state, priority, assignees, created_by, labels } = params;
|
const { state, priority, assignees, created_by, labels } = params;
|
||||||
|
|
||||||
@ -122,6 +124,12 @@ export const APP_INTEGRATIONS = "APP_INTEGRATIONS";
|
|||||||
export const WORKSPACE_INTEGRATIONS = (workspaceSlug: string) =>
|
export const WORKSPACE_INTEGRATIONS = (workspaceSlug: string) =>
|
||||||
`WORKSPACE_INTEGRATIONS_${workspaceSlug.toUpperCase()}`;
|
`WORKSPACE_INTEGRATIONS_${workspaceSlug.toUpperCase()}`;
|
||||||
|
|
||||||
|
export const JIRA_IMPORTER_DETAIL = (workspaceSlug: string, params: IJiraMetadata) => {
|
||||||
|
const { api_token, cloud_hostname, email, project_key } = params;
|
||||||
|
|
||||||
|
return `JIRA_IMPORTER_DETAIL_${workspaceSlug.toUpperCase()}_${api_token}_${cloud_hostname}_${email}_${project_key}`;
|
||||||
|
};
|
||||||
|
|
||||||
//import-export
|
//import-export
|
||||||
export const IMPORTER_SERVICES_LIST = (workspaceSlug: string) =>
|
export const IMPORTER_SERVICES_LIST = (workspaceSlug: string) =>
|
||||||
`IMPORTER_SERVICES_LIST_${workspaceSlug.toUpperCase()}`;
|
`IMPORTER_SERVICES_LIST_${workspaceSlug.toUpperCase()}`;
|
||||||
@ -153,6 +161,7 @@ export const PAGE_BLOCK_DETAILS = (pageId: string) => `PAGE_BLOCK_DETAILS_${page
|
|||||||
|
|
||||||
// estimates
|
// estimates
|
||||||
export const ESTIMATES_LIST = (projectId: string) => `ESTIMATES_LIST_${projectId.toUpperCase()}`;
|
export const ESTIMATES_LIST = (projectId: string) => `ESTIMATES_LIST_${projectId.toUpperCase()}`;
|
||||||
export const ESTIMATE_DETAILS = (estimateId: string) => `ESTIMATE_DETAILS_${estimateId.toUpperCase()}`;
|
export const ESTIMATE_DETAILS = (estimateId: string) =>
|
||||||
|
`ESTIMATE_DETAILS_${estimateId.toUpperCase()}`;
|
||||||
export const ESTIMATE_POINTS_LIST = (estimateId: string) =>
|
export const ESTIMATE_POINTS_LIST = (estimateId: string) =>
|
||||||
`ESTIMATES_POINTS_LIST_${estimateId.toUpperCase()}`;
|
`ESTIMATES_POINTS_LIST_${estimateId.toUpperCase()}`;
|
||||||
|
@ -75,11 +75,11 @@ export const IMPORTERS_EXPORTERS_LIST = [
|
|||||||
description: "Import issues from GitHub repositories and sync them.",
|
description: "Import issues from GitHub repositories and sync them.",
|
||||||
logo: GithubLogo,
|
logo: GithubLogo,
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// provider: "jira",
|
provider: "jira",
|
||||||
// type: "import",
|
type: "import",
|
||||||
// title: "Jira",
|
title: "Jira",
|
||||||
// description: "Import issues and epics from Jira projects and epics.",
|
description: "Import issues and epics from Jira projects and epics.",
|
||||||
// logo: JiraLogo,
|
logo: JiraLogo,
|
||||||
// },
|
},
|
||||||
];
|
];
|
||||||
|
43
apps/app/hooks/use-workspace-members.tsx
Normal file
43
apps/app/hooks/use-workspace-members.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import useSWR from "swr";
|
||||||
|
// services
|
||||||
|
import workspaceService from "services/workspace.service";
|
||||||
|
// fetch-keys
|
||||||
|
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||||
|
// hooks
|
||||||
|
import useUser from "./use-user";
|
||||||
|
|
||||||
|
const useWorkspaceMembers = (workspaceSlug?: string) => {
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
|
const { data: workspaceMembers, error: workspaceMemberErrors } = useSWR(
|
||||||
|
workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug) : null,
|
||||||
|
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug) : null
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasJoined = workspaceMembers?.some((item: any) => item.member.id === (user as any)?.id);
|
||||||
|
|
||||||
|
const isOwner = workspaceMembers?.some(
|
||||||
|
(item) => item.member.id === (user as any)?.id && item.role === 20
|
||||||
|
);
|
||||||
|
const isMember = workspaceMembers?.some(
|
||||||
|
(item) => item.member.id === (user as any)?.id && item.role === 15
|
||||||
|
);
|
||||||
|
const isViewer = workspaceMembers?.some(
|
||||||
|
(item) => item.member.id === (user as any)?.id && item.role === 10
|
||||||
|
);
|
||||||
|
const isGuest = workspaceMembers?.some(
|
||||||
|
(item) => item.member.id === (user as any)?.id && item.role === 5
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
workspaceMembers,
|
||||||
|
workspaceMemberErrors,
|
||||||
|
hasJoined,
|
||||||
|
isOwner,
|
||||||
|
isMember,
|
||||||
|
isViewer,
|
||||||
|
isGuest,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useWorkspaceMembers;
|
@ -56,6 +56,8 @@
|
|||||||
"eslint-config-custom": "*",
|
"eslint-config-custom": "*",
|
||||||
"eslint-config-next": "12.2.2",
|
"eslint-config-next": "12.2.2",
|
||||||
"postcss": "^8.4.14",
|
"postcss": "^8.4.14",
|
||||||
|
"prettier": "^2.8.7",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.2.7",
|
||||||
"tailwindcss": "^3.1.6",
|
"tailwindcss": "^3.1.6",
|
||||||
"tsconfig": "*",
|
"tsconfig": "*",
|
||||||
"typescript": "4.7.4"
|
"typescript": "4.7.4"
|
||||||
|
34
apps/app/services/integration/jira.service.ts
Normal file
34
apps/app/services/integration/jira.service.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import APIService from "services/api.service";
|
||||||
|
|
||||||
|
// types
|
||||||
|
import { IJiraMetadata, IJiraResponse, IJiraImporterForm } from "types";
|
||||||
|
|
||||||
|
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||||
|
|
||||||
|
class JiraImportedService extends APIService {
|
||||||
|
constructor() {
|
||||||
|
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
||||||
|
}
|
||||||
|
|
||||||
|
async getJiraProjectInfo(workspaceSlug: string, params: IJiraMetadata): Promise<IJiraResponse> {
|
||||||
|
return this.get(`/api/workspaces/${workspaceSlug}/importers/jira`, {
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async createJiraImporter(workspaceSlug: string, data: IJiraImporterForm): Promise<IJiraResponse> {
|
||||||
|
return this.post(`/api/workspaces/${workspaceSlug}/projects/importers/jira/`, data)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const jiraImporterService = new JiraImportedService();
|
||||||
|
|
||||||
|
export default jiraImporterService;
|
@ -1,4 +1,5 @@
|
|||||||
export * from "./github-importer";
|
export * from "./github-importer";
|
||||||
|
export * from "./jira-importer";
|
||||||
|
|
||||||
import { IProjectLite } from "types/projects";
|
import { IProjectLite } from "types/projects";
|
||||||
// types
|
// types
|
||||||
|
58
apps/app/types/importer/jira-importer.d.ts
vendored
Normal file
58
apps/app/types/importer/jira-importer.d.ts
vendored
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
export interface IJiraImporterForm {
|
||||||
|
metadata: IJiraMetadata;
|
||||||
|
config: IJiraConfig;
|
||||||
|
data: IJiraData;
|
||||||
|
project_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IJiraConfig {
|
||||||
|
epics_to_modules: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IJiraData {
|
||||||
|
users: User[];
|
||||||
|
invite_users: boolean;
|
||||||
|
total_issues: number;
|
||||||
|
total_labels: number;
|
||||||
|
total_states: number;
|
||||||
|
total_modules: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
username: string;
|
||||||
|
import: "invite" | "map" | false;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IJiraMetadata {
|
||||||
|
cloud_hostname: string;
|
||||||
|
api_token: string;
|
||||||
|
project_key: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IJiraResponse {
|
||||||
|
issues: number;
|
||||||
|
modules: number;
|
||||||
|
labels: number;
|
||||||
|
states: number;
|
||||||
|
users: IJiraResponseUser[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IJiraResponseUser {
|
||||||
|
self: string;
|
||||||
|
accountId: string;
|
||||||
|
accountType: string;
|
||||||
|
emailAddress: string;
|
||||||
|
avatarUrls: AvatarUrls;
|
||||||
|
displayName: string;
|
||||||
|
active: boolean;
|
||||||
|
locale: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IJiraResponseAvatarUrls {
|
||||||
|
"48x48": string;
|
||||||
|
"24x24": string;
|
||||||
|
"16x16": string;
|
||||||
|
"32x32": string;
|
||||||
|
}
|
10
yarn.lock
10
yarn.lock
@ -6485,6 +6485,16 @@ prelude-ls@^1.2.1:
|
|||||||
resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz"
|
resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz"
|
||||||
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
||||||
|
|
||||||
|
prettier-plugin-tailwindcss@^0.2.7:
|
||||||
|
version "0.2.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.2.7.tgz#2314d728cce9c9699ced41a01253eb48b4218da5"
|
||||||
|
integrity sha512-jQopIOgjLpX+y8HeD56XZw7onupRTC0cw7eKKUimI7vhjkPF5/1ltW5LyqaPtSyc8HvEpvNZsvvsGFa2qpa59w==
|
||||||
|
|
||||||
|
prettier@^2.8.7:
|
||||||
|
version "2.8.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.7.tgz#bb79fc8729308549d28fe3a98fce73d2c0656450"
|
||||||
|
integrity sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==
|
||||||
|
|
||||||
prettier@latest:
|
prettier@latest:
|
||||||
version "2.8.4"
|
version "2.8.4"
|
||||||
resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz"
|
resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz"
|
||||||
|
Loading…
Reference in New Issue
Block a user