New Directory Setup (#2065)

* chore: moved app & space from apps to root

* chore: modified workspace configuration

* chore: modified dockerfiles for space and web

* chore: modified icons for space

* feat: updated files for new svg icons supported by next-images

* chore: added /spaces base path for next

* chore: added compose config for space

* chore: updated husky configuration

* chore: updated workflows for new configuration

* chore: changed app name to web

* fix: resolved build errors with web

* chore: reset file tracing root for both projects

* chore: added nginx config for deploy

* fix: eslint and tsconfig settings for space app

* husky setup fixes based on new dir

* eslint fixes

* prettier formatting

---------

Co-authored-by: Henit Chobisa <chobisa.henit@gmail.com>
This commit is contained in:
sriram veeraghanta
2023-09-03 18:50:30 +05:30
committed by GitHub
parent 20e36194b4
commit 1e152c666c
1022 changed files with 1475 additions and 1240 deletions

View 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-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">Issues</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>
);
};

View File

@@ -0,0 +1,179 @@
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="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">
<a className="text-custom-primary underline" 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="font-semibold">Jira Project Key</h3>
<p className="text-sm text-custom-text-200">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="font-semibold">Jira Email Address</h3>
<p className="text-sm text-custom-text-200">
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="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">
<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="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
width="w-full"
onChange={onChange}
label={
<span>
{value && value !== "" ? (
projects?.find((p) => p.id === value)?.name
) : (
<span className="text-custom-text-200">Select a project</span>
)}
</span>
}
verticalPosition="top"
>
{projects && 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-custom-text-200">
<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-custom-text-200"
>
<PlusIcon className="h-4 w-4 text-custom-text-200" />
<span>Create new project</span>
</button>
</div>
</CustomSelect>
)}
/>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,150 @@
import { FC } from "react";
// next
import { useRouter } from "next/router";
// swr
import useSWR from "swr";
// react-hook-form
import { useFormContext, useFieldArray, Controller } from "react-hook-form";
// fetch keys
import { WORKSPACE_MEMBERS_WITH_EMAIL } from "constants/fetch-keys";
// services
import workspaceService from "services/workspace.service";
// 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 { data: members } = useSWR(
workspaceSlug ? WORKSPACE_MEMBERS_WITH_EMAIL(workspaceSlug?.toString() ?? "") : null,
workspaceSlug
? () => workspaceService.workspaceMembersWithEmail(workspaceSlug?.toString() ?? "")
: null
);
const options = members?.map((member) => ({
value: member.member.email,
query: member.member.display_name ?? "",
content: (
<div className="flex items-center gap-2">
<Avatar user={member.member} />
{member.member.display_name}
</div>
),
}));
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}
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>
);
};

View 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 { 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: "",
};

View 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-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">Issues</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>
);
};

View File

@@ -0,0 +1,230 @@
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, 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 { ICurrentUserResponse, IJiraImporterForm } from "types";
const integrationWorkflowData: Array<{
title: string;
key: TJiraIntegrationSteps;
icon: any;
}> = [
{
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,
},
];
type Props = {
user: ICurrentUserResponse | undefined;
};
export const JiraImporterRoot: React.FC<Props> = ({ user }) => {
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, user)
.then(() => {
mutate(IMPORTER_SERVICES_LIST(workspaceSlug.toString()));
router.push(`/${workspaceSlug}/settings/imports`);
})
.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/imports`}>
<div className="inline-flex cursor-pointer items-center gap-2 text-sm font-medium text-custom-text-200 hover:text-custom-text-100">
<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-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
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-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" && (
<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>
);
};