diff --git a/apps/app/components/project/single-integration-card.tsx b/apps/app/components/project/single-integration-card.tsx index 42220647b..dc2630a2e 100644 --- a/apps/app/components/project/single-integration-card.tsx +++ b/apps/app/components/project/single-integration-card.tsx @@ -1,16 +1,22 @@ +import React, { useState } from "react"; + import Image from "next/image"; import useSWR, { mutate } from "swr"; +import useSWRInfinite from "swr/infinite"; +// headless ui +import { Combobox, Transition } from "@headlessui/react"; // services import projectService from "services/project.service"; // hooks import { useRouter } from "next/router"; import useToast from "hooks/use-toast"; -// ui -import { CustomSelect } from "components/ui"; // icons +import { CheckIcon, ChevronDownIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import GithubLogo from "public/logos/github-square.png"; +// helpers +import { truncateText } from "helpers/string.helper"; // types import { IWorkspaceIntegrations } from "types"; // fetch-keys @@ -21,6 +27,8 @@ type Props = { }; export const SingleIntegration: React.FC<Props> = ({ integration }) => { + const [query, setQuery] = useState(""); + const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -38,11 +46,28 @@ export const SingleIntegration: React.FC<Props> = ({ integration }) => { : null ); - const { data: userRepositories } = useSWR("USER_REPOSITORIES", () => - workspaceSlug && integration - ? projectService.getGithubRepositories(workspaceSlug as any, integration.id) - : null - ); + const getKey = (pageIndex: number) => { + if (!workspaceSlug || !integration) return; + + return `${ + process.env.NEXT_PUBLIC_API_BASE_URL + }/api/workspaces/${workspaceSlug}/workspace-integrations/${ + integration.id + }/github-repositories/?page=${++pageIndex}`; + }; + + const fetchGithubRepos = async (url: string) => { + const data = await projectService.getGithubRepositories(url); + + return data; + }; + + const { + data: paginatedData, + size, + setSize, + isValidating, + } = useSWRInfinite(getKey, fetchGithubRepos); const handleChange = (repo: any) => { if (!workspaceSlug || !projectId || !integration) return; @@ -82,6 +107,21 @@ export const SingleIntegration: React.FC<Props> = ({ integration }) => { }); }; + const userRepositories = (paginatedData ?? []).map((data) => data.repositories).flat(); + const totalCount = paginatedData && paginatedData.length > 0 ? paginatedData[0].total_count : 0; + + const options = + userRepositories.map((repo) => ({ + value: repo.id, + query: repo.full_name, + content: <p>{truncateText(repo.full_name, 25)}</p>, + })) ?? []; + + const filteredOptions = + query === "" + ? options + : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); + return ( <> {integration && ( @@ -97,42 +137,95 @@ export const SingleIntegration: React.FC<Props> = ({ integration }) => { <p className="text-sm text-gray-400">Select GitHub repository to enable sync.</p> </div> </div> - <CustomSelect + <Combobox + as="div" value={ syncedGithubRepository && syncedGithubRepository.length > 0 ? `${syncedGithubRepository[0].repo_detail.owner}/${syncedGithubRepository[0].repo_detail.name}` : null } onChange={(val: string) => { - const repo = userRepositories?.repositories.find((repo) => repo.full_name === val); + const repo = userRepositories.find((repo) => repo.id === val); handleChange(repo); }} - label={ - syncedGithubRepository && syncedGithubRepository.length > 0 - ? `${syncedGithubRepository[0].repo_detail.owner}/${syncedGithubRepository[0].repo_detail.name}` - : "Select Repository" - } - input + className="relative flex-shrink-0 text-left" > - {userRepositories ? ( - userRepositories.repositories.length > 0 ? ( - userRepositories.repositories.map((repo) => ( - <CustomSelect.Option - key={repo.id} - value={repo.full_name} - className="flex items-center gap-2" - > - <>{repo.full_name}</> - </CustomSelect.Option> - )) - ) : ( - <p className="p-2 text-center text-xs text-gray-400">No repositories found</p> - ) - ) : ( - <p className="p-2 text-center text-xs text-gray-400">Loading repositories...</p> + {({ open }: any) => ( + <> + <Combobox.Button className="flex w-full cursor-pointer items-center justify-between gap-1 rounded-md border px-3 py-1.5 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"> + {syncedGithubRepository && syncedGithubRepository.length > 0 + ? `${syncedGithubRepository[0].repo_detail.owner}/${syncedGithubRepository[0].repo_detail.name}` + : "Select Repository"} + <ChevronDownIcon className="h-3 w-3" aria-hidden="true" /> + </Combobox.Button> + + <Transition + show={open} + as={React.Fragment} + enter="transition ease-out duration-200" + enterFrom="opacity-0 translate-y-1" + enterTo="opacity-100 translate-y-0" + leave="transition ease-in duration-150" + leaveFrom="opacity-100 translate-y-0" + leaveTo="opacity-0 translate-y-1" + > + <Combobox.Options className="absolute right-0 z-10 mt-1 min-w-[10rem] origin-top-right rounded-md bg-white p-2 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"> + <div className="flex w-full items-center justify-start rounded-sm border bg-gray-100 px-2 text-gray-500"> + <MagnifyingGlassIcon className="h-3 w-3" /> + <Combobox.Input + className="w-full bg-transparent py-1 px-2 text-xs focus:outline-none" + onChange={(e) => setQuery(e.target.value)} + placeholder="Type to search..." + displayValue={(assigned: any) => assigned?.name} + /> + </div> + <div className="vertical-scroll-enable mt-2 max-h-44 space-y-1 overflow-y-scroll"> + <p className="px-1 text-[0.6rem] text-gray-500"> + {options.length} of {totalCount} repositories + </p> + {paginatedData ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + <Combobox.Option + key={option.value} + value={option.value} + className={({ active, selected }) => + `${active || selected ? "bg-hover-gray" : ""} ${ + selected ? "font-medium" : "" + } flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 text-gray-500` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && <CheckIcon className="h-4 w-4" />} + </> + )} + </Combobox.Option> + )) + ) : ( + <p className="text-center text-gray-500">No matching results</p> + ) + ) : ( + <p className="text-center text-gray-500">Loading...</p> + )} + {userRepositories && options.length < totalCount && ( + <button + type="button" + className="w-full p-1 text-center text-[0.6rem] text-gray-500 hover:bg-hover-gray" + onClick={() => setSize(size + 1)} + disabled={isValidating} + > + {isValidating ? "Loading..." : "Click to load more..."} + </button> + )} + </div> + </Combobox.Options> + </Transition> + </> )} - </CustomSelect> + </Combobox> </div> )} </> diff --git a/apps/app/services/api.service.ts b/apps/app/services/api.service.ts index 112a87f9e..a625c0b37 100644 --- a/apps/app/services/api.service.ts +++ b/apps/app/services/api.service.ts @@ -39,6 +39,15 @@ abstract class APIService { }; } + getWithoutBase(url: string, config = {}): Promise<any> { + return axios({ + method: "get", + url: url, + headers: this.getAccessToken() ? this.getHeaders() : {}, + ...config, + }); + } + get(url: string, config = {}): Promise<any> { return axios({ method: "get", diff --git a/apps/app/services/project.service.ts b/apps/app/services/project.service.ts index cb280596e..ec07b7aa4 100644 --- a/apps/app/services/project.service.ts +++ b/apps/app/services/project.service.ts @@ -209,13 +209,8 @@ class ProjectServices extends APIService { }); } - async getGithubRepositories( - slug: string, - workspaceIntegrationId: string - ): Promise<GithubRepositoriesResponse> { - return this.get( - `/api/workspaces/${slug}/workspace-integrations/${workspaceIntegrationId}/github-repositories/` - ) + async getGithubRepositories(url: string): Promise<GithubRepositoriesResponse> { + return this.getWithoutBase(url) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; diff --git a/apps/app/styles/globals.css b/apps/app/styles/globals.css index e6490a822..f4dd63e48 100644 --- a/apps/app/styles/globals.css +++ b/apps/app/styles/globals.css @@ -30,34 +30,46 @@ -webkit-font-smoothing: antialiased; } -/* Horizontal Scrollbar style */ -.horizontal-scroll-enable{ - overflow-x: scroll; -} +/* scrollbar style */ -.horizontal-scroll-enable::-webkit-scrollbar{ - display: block; - height: 10px; -} - -.horizontal-scroll-enable::-webkit-scrollbar-thumb{ - border-radius: 5px; - background-color: #9ca3af; -} -/* end Horizontal Scrollbar style */ -.scrollbar-enable::-webkit-scrollbar { - display: block; -} - -/* Scrollbar style */ ::-webkit-scrollbar { display: none; } -.no-scrollbar::-webkit-scrollbar { - display: none; +.horizontal-scroll-enable { + overflow-x: scroll; } -/* End scrollbar style */ + +.horizontal-scroll-enable::-webkit-scrollbar { + display: block; + height: 7px; +} + +.horizontal-scroll-enable::-webkit-scrollbar-track { + height: 7px; + background-color: #f5f5f5; +} + +.horizontal-scroll-enable::-webkit-scrollbar-thumb { + border-radius: 5px; + background-color: #9ca3af; +} + +.vertical-scroll-enable::-webkit-scrollbar { + display: block; + width: 5px; +} + +.vertical-scroll-enable::-webkit-scrollbar-track { + width: 5px; + background-color: #f5f5f5; +} + +.vertical-scroll-enable::-webkit-scrollbar-thumb { + border-radius: 5px; + background-color: #9ca3af; +} +/* end scrollbar style */ .tags-input-container { border: 2px solid #000;