Merge pull request #138 from snowball-tools/ayungavis/T-4911-create-project-import-a-repository-section
[T-4911: feat] Re-styling and refactor create project import a repository section
This commit is contained in:
commit
3e42899f2e
@ -1,81 +0,0 @@
|
|||||||
import React, { useCallback } from 'react';
|
|
||||||
import toast from 'react-hot-toast';
|
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { Chip, IconButton, Spinner } from '@material-tailwind/react';
|
|
||||||
|
|
||||||
import { relativeTimeISO } from '../../../utils/time';
|
|
||||||
import { GitRepositoryDetails } from '../../../types';
|
|
||||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
|
||||||
import { GithubIcon, LockIcon } from 'components/shared/CustomIcon';
|
|
||||||
|
|
||||||
interface ProjectRepoCardProps {
|
|
||||||
repository: GitRepositoryDetails;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
|
|
||||||
const client = useGQLClient();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [isLoading, setIsLoading] = React.useState(false);
|
|
||||||
|
|
||||||
const { orgSlug } = useParams();
|
|
||||||
|
|
||||||
const createProject = useCallback(async () => {
|
|
||||||
if (!repository) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
const { addProject } = await client.addProject(orgSlug!, {
|
|
||||||
name: `${repository.owner!.login}-${repository.name}`,
|
|
||||||
prodBranch: repository.default_branch!,
|
|
||||||
repository: repository.full_name,
|
|
||||||
// TODO: Compute template from repo
|
|
||||||
template: 'webapp',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (Boolean(addProject)) {
|
|
||||||
setIsLoading(false);
|
|
||||||
navigate(`import?projectId=${addProject.id}`);
|
|
||||||
} else {
|
|
||||||
setIsLoading(false);
|
|
||||||
toast.error('Failed to create project');
|
|
||||||
}
|
|
||||||
}, [client, repository]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="group flex items-center gap-4 text-gray-500 text-xs hover:bg-gray-100 p-2 cursor-pointer"
|
|
||||||
onClick={createProject}
|
|
||||||
>
|
|
||||||
<div className="w-10 h-10 bg-white rounded-md justify-center items-center gap-1.5 inline-flex">
|
|
||||||
<GithubIcon />
|
|
||||||
</div>
|
|
||||||
<div className="grow">
|
|
||||||
<div>
|
|
||||||
<span className="text-black">{repository.full_name}</span>
|
|
||||||
{repository.visibility === 'private' && (
|
|
||||||
<Chip
|
|
||||||
className="normal-case inline ml-6 font-normal text-xs text-xs bg-orange-50 border border-orange-200 text-orange-600 items-center gap-1 inline-flex"
|
|
||||||
size="sm"
|
|
||||||
value="Private"
|
|
||||||
icon={<LockIcon />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p>{repository.updated_at && relativeTimeISO(repository.updated_at)}</p>
|
|
||||||
</div>
|
|
||||||
{isLoading ? (
|
|
||||||
<Spinner className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<div className="hidden group-hover:block">
|
|
||||||
<IconButton size="sm" placeholder={''}>
|
|
||||||
{'>'}
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProjectRepoCard;
|
|
@ -0,0 +1,114 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Spinner } from '@material-tailwind/react';
|
||||||
|
|
||||||
|
import { relativeTimeISO } from 'utils/time';
|
||||||
|
import { GitRepositoryDetails } from 'types';
|
||||||
|
import { useGQLClient } from 'context/GQLClientContext';
|
||||||
|
import {
|
||||||
|
ArrowRightCircleIcon,
|
||||||
|
GithubIcon,
|
||||||
|
LockIcon,
|
||||||
|
} from 'components/shared/CustomIcon';
|
||||||
|
import { Button } from 'components/shared/Button';
|
||||||
|
import { useToast } from 'components/shared/Toast';
|
||||||
|
|
||||||
|
interface ProjectRepoCardProps {
|
||||||
|
repository: GitRepositoryDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({
|
||||||
|
repository,
|
||||||
|
}) => {
|
||||||
|
const client = useGQLClient();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const { orgSlug } = useParams();
|
||||||
|
const { toast, dismiss } = useToast();
|
||||||
|
|
||||||
|
const createProject = useCallback(async () => {
|
||||||
|
if (!repository || !orgSlug) {
|
||||||
|
return toast({
|
||||||
|
id: 'missing-repository-or-org-slug',
|
||||||
|
title: 'Repository or organization slug is missing',
|
||||||
|
variant: 'error',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const { addProject } = await client.addProject(orgSlug, {
|
||||||
|
name: `${repository.owner?.login}-${repository.name}`,
|
||||||
|
prodBranch: repository.default_branch as string,
|
||||||
|
repository: repository.full_name,
|
||||||
|
// TODO: Compute template from repo
|
||||||
|
template: 'webapp',
|
||||||
|
});
|
||||||
|
if (addProject) {
|
||||||
|
navigate(`import?projectId=${addProject.id}`);
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
id: 'failed-to-create-project',
|
||||||
|
title: 'Failed to create project',
|
||||||
|
variant: 'error',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast({
|
||||||
|
id: 'failed-to-create-project',
|
||||||
|
title: 'Failed to create project',
|
||||||
|
variant: 'error',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [client, repository, orgSlug, setIsLoading, navigate, toast]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="group flex items-start sm:items-center gap-3 px-3 py-3 cursor-pointer rounded-xl hover:bg-base-bg-emphasized relative"
|
||||||
|
onClick={createProject}
|
||||||
|
>
|
||||||
|
{/* Icon container */}
|
||||||
|
<div className="w-10 h-10 bg-base-bg rounded-md justify-center items-center flex">
|
||||||
|
<GithubIcon />
|
||||||
|
</div>
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex flex-1 gap-3 flex-wrap">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<p className="text-elements-high-em text-sm font-medium tracking-[-0.006em]">
|
||||||
|
{repository.full_name}
|
||||||
|
</p>
|
||||||
|
<p className="text-elements-low-em text-xs">
|
||||||
|
{repository.updated_at && relativeTimeISO(repository.updated_at)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{repository.visibility === 'private' && (
|
||||||
|
<div className="bg-orange-50 border border-orange-200 px-2 py-1 flex items-center gap-1 rounded-lg text-xs text-orange-600 h-fit">
|
||||||
|
<LockIcon />
|
||||||
|
Private
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Right action */}
|
||||||
|
{isLoading ? (
|
||||||
|
<Spinner className="h-4 w-4 absolute right-3" />
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="tertiary"
|
||||||
|
size="sm"
|
||||||
|
iconOnly
|
||||||
|
className="sm:group-hover:flex hidden absolute right-3"
|
||||||
|
>
|
||||||
|
<ArrowRightCircleIcon />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export * from './ProjectRepoCard';
|
@ -3,13 +3,17 @@ import { Octokit } from 'octokit';
|
|||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import { useDebounce } from 'usehooks-ts';
|
import { useDebounce } from 'usehooks-ts';
|
||||||
|
|
||||||
import { Button, Typography, Option } from '@material-tailwind/react';
|
import { Button, Typography } from '@material-tailwind/react';
|
||||||
|
|
||||||
import SearchBar from '../../SearchBar';
|
import { ProjectRepoCard } from 'components/projects/create/ProjectRepoCard';
|
||||||
import ProjectRepoCard from './ProjectRepoCard';
|
import { GitOrgDetails, GitRepositoryDetails } from 'types';
|
||||||
import { GitOrgDetails, GitRepositoryDetails } from '../../../types';
|
import {
|
||||||
import AsyncSelect from '../../shared/AsyncSelect';
|
ChevronGrabberHorizontal,
|
||||||
import { GithubIcon } from 'components/shared/CustomIcon';
|
GithubIcon,
|
||||||
|
SearchIcon,
|
||||||
|
} from 'components/shared/CustomIcon';
|
||||||
|
import { Select, SelectOption } from 'components/shared/Select';
|
||||||
|
import { Input } from 'components/shared/Input';
|
||||||
|
|
||||||
const DEFAULT_SEARCHED_REPO = '';
|
const DEFAULT_SEARCHED_REPO = '';
|
||||||
const REPOS_PER_PAGE = 5;
|
const REPOS_PER_PAGE = 5;
|
||||||
@ -18,9 +22,9 @@ interface RepositoryListProps {
|
|||||||
octokit: Octokit;
|
octokit: Octokit;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
export const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||||
const [searchedRepo, setSearchedRepo] = useState(DEFAULT_SEARCHED_REPO);
|
const [searchedRepo, setSearchedRepo] = useState(DEFAULT_SEARCHED_REPO);
|
||||||
const [selectedAccount, setSelectedAccount] = useState('');
|
const [selectedAccount, setSelectedAccount] = useState<SelectOption>();
|
||||||
const [orgs, setOrgs] = useState<GitOrgDetails[]>([]);
|
const [orgs, setOrgs] = useState<GitOrgDetails[]>([]);
|
||||||
// TODO: Add new type for Git user when required
|
// TODO: Add new type for Git user when required
|
||||||
const [gitUser, setGitUser] = useState<GitOrgDetails>();
|
const [gitUser, setGitUser] = useState<GitOrgDetails>();
|
||||||
@ -35,7 +39,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
|||||||
const orgs = await octokit.rest.orgs.listForAuthenticatedUser();
|
const orgs = await octokit.rest.orgs.listForAuthenticatedUser();
|
||||||
setOrgs(orgs.data);
|
setOrgs(orgs.data);
|
||||||
setGitUser(user.data);
|
setGitUser(user.data);
|
||||||
setSelectedAccount(user.data.login);
|
setSelectedAccount({ label: user.data.login, value: user.data.login });
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchUserAndOrgs();
|
fetchUserAndOrgs();
|
||||||
@ -54,7 +58,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
|||||||
let query = `${debouncedSearchedRepo} in:name fork:true`;
|
let query = `${debouncedSearchedRepo} in:name fork:true`;
|
||||||
|
|
||||||
// Check if selected account is an organization
|
// Check if selected account is an organization
|
||||||
if (selectedAccount === gitUser.login) {
|
if (selectedAccount.value === gitUser.login) {
|
||||||
query = query + ` user:${selectedAccount}`;
|
query = query + ` user:${selectedAccount}`;
|
||||||
} else {
|
} else {
|
||||||
query = query + ` org:${selectedAccount}`;
|
query = query + ` org:${selectedAccount}`;
|
||||||
@ -69,7 +73,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedAccount === gitUser.login) {
|
if (selectedAccount.value === gitUser.login) {
|
||||||
const result = await octokit.rest.repos.listForAuthenticatedUser({
|
const result = await octokit.rest.repos.listForAuthenticatedUser({
|
||||||
per_page: REPOS_PER_PAGE,
|
per_page: REPOS_PER_PAGE,
|
||||||
affiliation: 'owner',
|
affiliation: 'owner',
|
||||||
@ -78,7 +82,9 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedOrg = orgs.find((org) => org.login === selectedAccount);
|
const selectedOrg = orgs.find(
|
||||||
|
(org) => org.login === selectedAccount.value,
|
||||||
|
);
|
||||||
assert(selectedOrg, 'Selected org not found in list');
|
assert(selectedOrg, 'Selected org not found in list');
|
||||||
|
|
||||||
const result = await octokit.rest.repos.listForOrg({
|
const result = await octokit.rest.repos.listForOrg({
|
||||||
@ -96,7 +102,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
|||||||
const handleResetFilters = useCallback(() => {
|
const handleResetFilters = useCallback(() => {
|
||||||
assert(gitUser, 'Git user is not available');
|
assert(gitUser, 'Git user is not available');
|
||||||
setSearchedRepo(DEFAULT_SEARCHED_REPO);
|
setSearchedRepo(DEFAULT_SEARCHED_REPO);
|
||||||
setSelectedAccount(gitUser.login);
|
setSelectedAccount({ label: gitUser.login, value: gitUser.login });
|
||||||
}, [gitUser]);
|
}, [gitUser]);
|
||||||
|
|
||||||
const accounts = useMemo(() => {
|
const accounts = useMemo(() => {
|
||||||
@ -107,35 +113,52 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
|||||||
return [gitUser, ...orgs];
|
return [gitUser, ...orgs];
|
||||||
}, [octokit, orgs, gitUser]);
|
}, [octokit, orgs, gitUser]);
|
||||||
|
|
||||||
|
const options = useMemo(() => {
|
||||||
|
return accounts.map((account) => ({
|
||||||
|
label: account.login,
|
||||||
|
value: account.login,
|
||||||
|
leftIcon: <GithubIcon />,
|
||||||
|
}));
|
||||||
|
}, [accounts]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<section className="space-y-3">
|
||||||
<div className="flex gap-2 mb-2 items-center">
|
{/* Dropdown and search */}
|
||||||
<div className="basis-1/3">
|
<div className="flex flex-col lg:flex-row gap-0 lg:gap-3 items-center">
|
||||||
<AsyncSelect
|
<div className="lg:basis-1/3 w-full">
|
||||||
|
<Select
|
||||||
|
options={options}
|
||||||
|
placeholder="Select a repository"
|
||||||
value={selectedAccount}
|
value={selectedAccount}
|
||||||
onChange={(value) => setSelectedAccount(value!)}
|
leftIcon={selectedAccount ? <GithubIcon /> : undefined}
|
||||||
>
|
rightIcon={<ChevronGrabberHorizontal />}
|
||||||
{accounts.map((account) => (
|
onChange={(value) => setSelectedAccount(value as SelectOption)}
|
||||||
<Option key={account.id} value={account.login}>
|
/>
|
||||||
<div className="flex items-center gap-2 justify-start">
|
|
||||||
<GithubIcon /> {account.login}
|
|
||||||
</div>
|
</div>
|
||||||
</Option>
|
<div className="basis-2/3 flex w-full flex-grow">
|
||||||
))}
|
<Input
|
||||||
</AsyncSelect>
|
className="w-full"
|
||||||
</div>
|
|
||||||
<div className="basis-2/3 flex-grow flex items-center">
|
|
||||||
<SearchBar
|
|
||||||
value={searchedRepo}
|
value={searchedRepo}
|
||||||
onChange={(event) => setSearchedRepo(event.target.value)}
|
|
||||||
placeholder="Search for repository"
|
placeholder="Search for repository"
|
||||||
|
leftIcon={<SearchIcon />}
|
||||||
|
onChange={(e) => setSearchedRepo(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Repository list */}
|
||||||
{Boolean(repositoryDetails.length) ? (
|
{Boolean(repositoryDetails.length) ? (
|
||||||
repositoryDetails.map((repo, key) => {
|
<div className="flex flex-col gap-2">
|
||||||
return <ProjectRepoCard repository={repo} key={key} />;
|
{repositoryDetails.map((repo, index) => (
|
||||||
})
|
<>
|
||||||
|
<ProjectRepoCard repository={repo} key={index} />
|
||||||
|
{/* Horizontal line */}
|
||||||
|
{index !== repositoryDetails.length - 1 && (
|
||||||
|
<div className="border-b border-border-separator/[0.06] w-full" />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-4 p-6 flex items-center justify-center">
|
<div className="mt-4 p-6 flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
@ -151,8 +174,6 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RepositoryList;
|
|
@ -0,0 +1 @@
|
|||||||
|
export * from './RepositoryList';
|
@ -55,9 +55,9 @@ export const TemplateCard: React.FC<TemplateCardProps> = ({
|
|||||||
}, [orgSlug, dismiss, isGitAuth, navigate, template, toast]);
|
}, [orgSlug, dismiss, isGitAuth, navigate, template, toast]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-3 px-3 py-3 bg-base-bg-alternate hover:bg-base-bg-emphasized rounded-2xl group relative',
|
'flex items-center gap-3 px-3 py-3 bg-base-bg-alternate hover:bg-base-bg-emphasized rounded-2xl group relative cursor-pointer',
|
||||||
{
|
{
|
||||||
'cursor-default': template?.isComingSoon,
|
'cursor-default': template?.isComingSoon,
|
||||||
},
|
},
|
||||||
@ -86,6 +86,6 @@ export const TemplateCard: React.FC<TemplateCardProps> = ({
|
|||||||
<ArrowRightCircleIcon />
|
<ArrowRightCircleIcon />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</button>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -8,6 +8,8 @@ export const inputTheme = tv(
|
|||||||
'items-center',
|
'items-center',
|
||||||
'rounded-lg',
|
'rounded-lg',
|
||||||
'relative',
|
'relative',
|
||||||
|
'gap-2',
|
||||||
|
'w-full',
|
||||||
'placeholder:text-elements-disabled',
|
'placeholder:text-elements-disabled',
|
||||||
'disabled:cursor-not-allowed',
|
'disabled:cursor-not-allowed',
|
||||||
'disabled:bg-controls-disabled',
|
'disabled:bg-controls-disabled',
|
||||||
@ -27,7 +29,7 @@ export const inputTheme = tv(
|
|||||||
'disabled:shadow-none',
|
'disabled:shadow-none',
|
||||||
'disabled:border-none',
|
'disabled:border-none',
|
||||||
],
|
],
|
||||||
icon: ['text-elements-mid-em'],
|
icon: ['text-elements-low-em'],
|
||||||
iconContainer: [
|
iconContainer: [
|
||||||
'absolute',
|
'absolute',
|
||||||
'inset-y-0',
|
'inset-y-0',
|
||||||
|
@ -87,12 +87,12 @@ export const Input = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
{renderLabels}
|
{renderLabels}
|
||||||
<div className={containerCls({ class: className })}>
|
<div className={containerCls({ class: className })}>
|
||||||
{leftIcon && renderLeftIcon}
|
{leftIcon && renderLeftIcon}
|
||||||
<input
|
<input
|
||||||
className={cn(inputCls({ class: 'w-80' }), {
|
className={cn(inputCls(), {
|
||||||
'pl-10': leftIcon,
|
'pl-10': leftIcon,
|
||||||
})}
|
})}
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -85,7 +85,7 @@ export const selectTheme = tv({
|
|||||||
size: {
|
size: {
|
||||||
md: {
|
md: {
|
||||||
container: ['min-h-11'],
|
container: ['min-h-11'],
|
||||||
inputWrapper: ['min-h-11', 'text-sm', 'pl-4', 'pr-4', 'py-1'],
|
inputWrapper: ['min-h-11', 'text-sm', 'pl-4', 'pr-4'],
|
||||||
icon: ['h-[18px]', 'w-[18px]'],
|
icon: ['h-[18px]', 'w-[18px]'],
|
||||||
helperText: 'text-sm',
|
helperText: 'text-sm',
|
||||||
helperIcon: ['h-5', 'w-5'],
|
helperIcon: ['h-5', 'w-5'],
|
||||||
@ -93,7 +93,7 @@ export const selectTheme = tv({
|
|||||||
},
|
},
|
||||||
sm: {
|
sm: {
|
||||||
container: ['min-h-8'],
|
container: ['min-h-8'],
|
||||||
inputWrapper: ['min-h-8', 'text-xs', 'pl-3', 'pr-3', 'py-0.5'],
|
inputWrapper: ['min-h-8', 'text-xs', 'pl-3', 'pr-3'],
|
||||||
icon: ['h-4', 'w-4'],
|
icon: ['h-4', 'w-4'],
|
||||||
helperText: 'text-xs',
|
helperText: 'text-xs',
|
||||||
helperIcon: ['h-4', 'w-4'],
|
helperIcon: ['h-4', 'w-4'],
|
||||||
|
@ -3,7 +3,6 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
ComponentPropsWithoutRef,
|
ComponentPropsWithoutRef,
|
||||||
useMemo,
|
useMemo,
|
||||||
useCallback,
|
|
||||||
MouseEvent,
|
MouseEvent,
|
||||||
useRef,
|
useRef,
|
||||||
useEffect,
|
useEffect,
|
||||||
@ -135,7 +134,9 @@ export const Select = ({
|
|||||||
const theme = selectTheme({ size, error, variant, orientation });
|
const theme = selectTheme({ size, error, variant, orientation });
|
||||||
|
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const [selectedItem, setSelectedItem] = useState<SelectOption | null>(null);
|
const [selectedItem, setSelectedItem] = useState<SelectOption | null>(
|
||||||
|
(value as SelectOption) || null,
|
||||||
|
);
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
const [dropdownPosition, setDropdownPosition] = useState<'top' | 'bottom'>(
|
const [dropdownPosition, setDropdownPosition] = useState<'top' | 'bottom'>(
|
||||||
'bottom',
|
'bottom',
|
||||||
@ -166,22 +167,6 @@ export const Select = ({
|
|||||||
}
|
}
|
||||||
}, [dropdownOpen]); // Re-calculate whenever the dropdown is opened
|
}, [dropdownOpen]); // Re-calculate whenever the dropdown is opened
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// If multiple selection is enabled, ensure the internal state is an array
|
|
||||||
if (multiple) {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
// Directly use the provided array
|
|
||||||
setSelectedItems(value);
|
|
||||||
} else {
|
|
||||||
// Reset or set to empty array if the value is not an array
|
|
||||||
setSelectedItems([]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// For single selection, directly set the selected item
|
|
||||||
setSelectedItem(value as SelectOption);
|
|
||||||
}
|
|
||||||
}, [value, multiple]);
|
|
||||||
|
|
||||||
const handleSelectedItemChange = (selectedItem: SelectOption | null) => {
|
const handleSelectedItemChange = (selectedItem: SelectOption | null) => {
|
||||||
setSelectedItem(selectedItem);
|
setSelectedItem(selectedItem);
|
||||||
setInputValue(selectedItem ? selectedItem.label : '');
|
setInputValue(selectedItem ? selectedItem.label : '');
|
||||||
@ -194,9 +179,9 @@ export const Select = ({
|
|||||||
addSelectedItem,
|
addSelectedItem,
|
||||||
removeSelectedItem,
|
removeSelectedItem,
|
||||||
selectedItems,
|
selectedItems,
|
||||||
setSelectedItems,
|
|
||||||
reset,
|
reset,
|
||||||
} = useMultipleSelection<SelectOption>({
|
} = useMultipleSelection<SelectOption>({
|
||||||
|
selectedItems: multiple ? (value as SelectOption[]) : [],
|
||||||
onSelectedItemsChange: multiple
|
onSelectedItemsChange: multiple
|
||||||
? undefined
|
? undefined
|
||||||
: ({ selectedItems }) => {
|
: ({ selectedItems }) => {
|
||||||
@ -234,6 +219,7 @@ export const Select = ({
|
|||||||
openMenu,
|
openMenu,
|
||||||
} = useCombobox({
|
} = useCombobox({
|
||||||
items: filteredItems,
|
items: filteredItems,
|
||||||
|
selectedItem: multiple ? null : (value as SelectOption) || null,
|
||||||
// @ts-expect-error – there are two params but we don't need the second one
|
// @ts-expect-error – there are two params but we don't need the second one
|
||||||
isItemDisabled: (item) => item.disabled,
|
isItemDisabled: (item) => item.disabled,
|
||||||
onInputValueChange: ({ inputValue = '' }) => setInputValue(inputValue),
|
onInputValueChange: ({ inputValue = '' }) => setInputValue(inputValue),
|
||||||
@ -265,16 +251,12 @@ export const Select = ({
|
|||||||
setInputValue('');
|
setInputValue('');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
selectedItem: multiple ? null : selectedItem,
|
|
||||||
// TODO: Make the input value empty when the dropdown is open, has a value, it is not multiple, and searchable
|
// TODO: Make the input value empty when the dropdown is open, has a value, it is not multiple, and searchable
|
||||||
itemToString: (item) => (item && !multiple ? item.label : ''),
|
itemToString: (item) => (item && !multiple ? item.label : ''),
|
||||||
});
|
});
|
||||||
|
|
||||||
const isSelected = useCallback(
|
const isSelected = (item: SelectOption) =>
|
||||||
(item: SelectOption) =>
|
multiple ? selectedItems.includes(item) : selectedItem === item;
|
||||||
multiple ? selectedItems.includes(item) : selectedItem === item,
|
|
||||||
[selectedItems, selectedItem, multiple],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleClear = (e: MouseEvent<SVGSVGElement, globalThis.MouseEvent>) => {
|
const handleClear = (e: MouseEvent<SVGSVGElement, globalThis.MouseEvent>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -336,6 +318,7 @@ export const Select = ({
|
|||||||
const isMultipleHasValue = multiple && selectedItems.length > 0;
|
const isMultipleHasValue = multiple && selectedItems.length > 0;
|
||||||
const isMultipleHasValueButNotSearchable =
|
const isMultipleHasValueButNotSearchable =
|
||||||
multiple && !searchable && selectedItems.length > 0;
|
multiple && !searchable && selectedItems.length > 0;
|
||||||
|
|
||||||
const displayPlaceholder = useMemo(() => {
|
const displayPlaceholder = useMemo(() => {
|
||||||
if (hideValues && isMultipleHasValue) {
|
if (hideValues && isMultipleHasValue) {
|
||||||
return `${selectedItems.length} selected`;
|
return `${selectedItems.length} selected`;
|
||||||
@ -391,6 +374,8 @@ export const Select = ({
|
|||||||
'w-6': isMultipleHasValueButNotSearchable && !hideValues,
|
'w-6': isMultipleHasValueButNotSearchable && !hideValues,
|
||||||
// Add margin to the X icon
|
// Add margin to the X icon
|
||||||
'ml-6': isMultipleHasValueButNotSearchable && clearable,
|
'ml-6': isMultipleHasValueButNotSearchable && clearable,
|
||||||
|
// Add padding if there's a left icon
|
||||||
|
'pl-7': leftIcon,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import templates from 'assets/templates';
|
import templates from 'assets/templates';
|
||||||
import RepositoryList from 'components/projects/create/RepositoryList';
|
import { RepositoryList } from 'components/projects/create/RepositoryList';
|
||||||
import ConnectAccount from 'components/projects/create/ConnectAccount';
|
import ConnectAccount from 'components/projects/create/ConnectAccount';
|
||||||
import { useOctokit } from 'context/OctokitContext';
|
import { useOctokit } from 'context/OctokitContext';
|
||||||
import { Heading } from 'components/shared/Heading';
|
import { Heading } from 'components/shared/Heading';
|
||||||
@ -13,8 +13,8 @@ const NewProject = () => {
|
|||||||
return isAuth ? (
|
return isAuth ? (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Heading as="h3" className="font-medium text-lg">
|
<Heading as="h3" className="font-medium text-lg pl-1">
|
||||||
Start with template
|
Start with a template
|
||||||
</Heading>
|
</Heading>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-3">
|
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-3">
|
||||||
{templates.map((template) => {
|
{templates.map((template) => {
|
||||||
@ -28,7 +28,7 @@ const NewProject = () => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Heading as="h3" className="font-medium text-lg mt-10">
|
<Heading as="h3" className="font-medium text-lg mt-10 pl-1 mb-3">
|
||||||
Import a repository
|
Import a repository
|
||||||
</Heading>
|
</Heading>
|
||||||
<RepositoryList octokit={octokit} />
|
<RepositoryList octokit={octokit} />
|
||||||
|
Loading…
Reference in New Issue
Block a user