[1/n][project settings ui] GeneralTabPanel (#23)

This commit is contained in:
Vivian Phung 2024-05-14 15:38:44 -04:00 committed by GitHub
commit 57956ec269
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 156 additions and 126 deletions

View File

@ -0,0 +1,20 @@
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const CopyUnfilledIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
{...props}
>
<path
d="M6 5.625V5.025C6 4.18492 6 3.76488 6.16349 3.44401C6.3073 3.16177 6.53677 2.9323 6.81901 2.78849C7.13988 2.625 7.55992 2.625 8.4 2.625H12.975C13.8151 2.625 14.2351 2.625 14.556 2.78849C14.8382 2.9323 15.0677 3.16177 15.2115 3.44401C15.375 3.76488 15.375 4.18492 15.375 5.025V9.975C15.375 10.8151 15.375 11.2351 15.2115 11.556C15.0677 11.8382 14.8382 12.0677 14.556 12.2115C14.2351 12.375 13.8151 12.375 12.975 12.375H12.375M12.375 8.025V12.975C12.375 13.8151 12.375 14.2351 12.2115 14.556C12.0677 14.8382 11.8382 15.0677 11.556 15.2115C11.2351 15.375 10.8151 15.375 9.975 15.375H5.025C4.18492 15.375 3.76488 15.375 3.44401 15.2115C3.16177 15.0677 2.9323 14.8382 2.78849 14.556C2.625 14.2351 2.625 13.8151 2.625 12.975V8.025C2.625 7.18492 2.625 6.76488 2.78849 6.44401C2.9323 6.16177 3.16177 5.9323 3.44401 5.78849C3.76488 5.625 4.18492 5.625 5.025 5.625H9.975C10.8151 5.625 11.2351 5.625 11.556 5.78849C11.8382 5.9323 12.0677 6.16177 12.2115 6.44401C12.375 6.76488 12.375 7.18492 12.375 8.025Z"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
/>
</CustomIcon>
);
};

View File

@ -0,0 +1,20 @@
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const TrashIcon: React.FC<CustomIconProps> = (props) => {
return (
<CustomIcon
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
{...props}
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M5.05612 3.33398C5.52062 2.16265 6.66341 1.33398 8.00079 1.33398C9.33816 1.33398 10.481 2.16265 10.9455 3.33398H14.1668C14.443 3.33398 14.6668 3.55784 14.6668 3.83398C14.6668 4.11013 14.443 4.33398 14.1668 4.33398H13.3023L12.7463 12.952C12.684 13.9167 11.8834 14.6673 10.9167 14.6673H5.08358C4.11688 14.6673 3.31629 13.9167 3.25405 12.952L2.69805 4.33398H1.8335C1.55735 4.33398 1.3335 4.11013 1.3335 3.83398C1.3335 3.55784 1.55735 3.33398 1.8335 3.33398H5.05612ZM6.17457 3.33398C6.55973 2.73248 7.23408 2.33398 8.00079 2.33398C8.76749 2.33398 9.44184 2.73248 9.827 3.33398H6.17457ZM7.00016 7.16732C7.00016 6.89118 6.77631 6.66732 6.50016 6.66732C6.22402 6.66732 6.00016 6.89118 6.00016 7.16732V10.834C6.00016 11.1101 6.22402 11.334 6.50016 11.334C6.77631 11.334 7.00016 11.1101 7.00016 10.834V7.16732ZM9.50016 6.66732C9.77631 6.66732 10.0002 6.89118 10.0002 7.16732V10.834C10.0002 11.1101 9.77631 11.334 9.50016 11.334C9.22402 11.334 9.00016 11.1101 9.00016 10.834V7.16732C9.00016 6.89118 9.22402 6.66732 9.50016 6.66732Z"
fill="currentColor"
/>
</CustomIcon>
);
};

View File

@ -67,6 +67,8 @@ export * from './AppleIcon';
export * from './CalendarDaysIcon';
export * from './GoogleIcon';
export * from './KeyIcon';
export * from './TrashIcon';
export * from './CopyUnfilledIcon';
// Templates
export * from './templates';

View File

@ -1,54 +1,35 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import { Link, useOutletContext } from 'react-router-dom';
import { useForm, Controller } from 'react-hook-form';
import toast from 'react-hot-toast';
import { Organization } from 'gql-client';
import {
Button,
Typography,
Input,
Option,
} from '@snowballtools/material-tailwind-react-fork';
import { useOutletContext } from 'react-router-dom';
import { useForm } from 'react-hook-form';
import DeleteProjectDialog from 'components/projects/project/settings/DeleteProjectDialog';
import { useGQLClient } from 'context/GQLClientContext';
import AsyncSelect from 'components/shared/AsyncSelect';
import { OutletContextType } from '../../../../../types/types';
import { OutletContextType } from '../../../../../types';
import { TransferProjectDialog } from 'components/projects/Dialog/TransferProjectDialog';
const CopyIcon = ({ value }: { value: string }) => {
return (
<span
onClick={() => {
navigator.clipboard.writeText(value);
toast.success('Project ID copied');
}}
className="cursor-pointer"
>
^
</span>
);
};
import { Input } from 'components/shared/Input';
import { Heading } from 'components/shared/Heading';
import { Button } from 'components/shared/Button';
import { Select, SelectOption } from 'components/shared/Select';
import { TrashIcon, CopyUnfilledIcon } from 'components/shared/CustomIcon';
import { useToast } from 'components/shared/Toast';
const GeneralTabPanel = () => {
const client = useGQLClient();
const { toast } = useToast();
const { project, onUpdate } = useOutletContext<OutletContextType>();
const [transferOrganizations, setTransferOrganizations] = useState<
Organization[]
SelectOption[]
>([]);
const [selectedTransferOrganization, setSelectedTransferOrganization] =
useState('');
useState<SelectOption>();
const {
handleSubmit: handleTransfer,
control,
formState,
reset: transferFormReset,
} = useForm({
const { handleSubmit: handleTransfer, reset: transferFormReset } = useForm({
defaultValues: {
orgId: '',
org: {
value: '',
label: '',
},
},
});
@ -75,33 +56,47 @@ const GeneralTabPanel = () => {
const orgsToTransfer = organizations.filter(
(org) => org.id !== project.organization.id,
);
setTransferOrganizations(orgsToTransfer);
const selectableOrgs: SelectOption[] = orgsToTransfer.map((org) => ({
value: org.id,
label: org.name,
}));
setTransferOrganizations(selectableOrgs);
}, [project]);
const handleTransferProject = useCallback(async () => {
const { updateProject: isTransferred } = await client.updateProject(
project.id,
{
organizationId: selectedTransferOrganization,
organizationId: selectedTransferOrganization?.value,
},
);
setOpenTransferDialog(!openTransferDialog);
if (isTransferred) {
toast.success('Project transferred');
toast({
id: 'project_transferred',
title: 'Project transferred successfully',
variant: 'success',
onDismiss() {},
});
await fetchUserOrganizations();
await onUpdate();
transferFormReset();
} else {
toast.error('Project not transrfered');
toast({
id: 'project_transfer_failed',
title: 'Project transfer failed',
variant: 'error',
onDismiss() {},
});
}
}, [project, selectedTransferOrganization]);
const selectedUserOrgName = useMemo(() => {
return (
transferOrganizations.find(
(org) => org.id === selectedTransferOrganization,
)?.name || ''
transferOrganizations.find((org) => org === selectedTransferOrganization)
?.label || ''
);
}, [transferOrganizations, selectedTransferOrganization]);
@ -114,7 +109,7 @@ const GeneralTabPanel = () => {
}, [project]);
return (
<>
<div className="flex-col justify-start items-start gap-6 inline-flex">
<form
onSubmit={handleSubmit(async ({ appName, description }) => {
const { updateProject } = await client.updateProject(project.id, {
@ -125,85 +120,74 @@ const GeneralTabPanel = () => {
await onUpdate();
}
})}
className="self-stretch space-y-3 px-2"
>
<Typography variant="h6">Project info</Typography>
<Typography variant="small" className="font-medium text-gray-800">
App name
</Typography>
<Heading className="text-sky-950 text-lg font-medium leading-normal">
Project Info
</Heading>
<Input
variant="outlined"
// TODO: Debug issue: https://github.com/creativetimofficial/material-tailwind/issues/427
label="App name"
size="md"
{...register('appName')}
/>
<Typography variant="small" className="font-medium text-gray-800">
Description (Optional)
</Typography>
<Input variant="outlined" size="md" {...register('description')} />
<Typography variant="small" className="font-medium text-gray-800">
Project ID
</Typography>
<Input
variant="outlined"
size="md"
label="Description (Optional)"
{...register('description')}
/>
<div
onClick={() => {
navigator.clipboard.writeText(project.id);
toast({
id: 'copied_project_id',
title: 'Project ID copied to clipboard',
variant: 'success',
onDismiss() {},
});
}}
>
<Input
value={project.id}
size="md"
disabled
icon={<CopyIcon value={project.id} />}
label="Project ID"
rightIcon={<CopyUnfilledIcon />}
/>
</div>
<Button
type="submit"
variant="gradient"
size="sm"
className="mt-1"
size="md"
disabled={!updateProjectFormState.isDirty}
>
Save
</Button>
</form>
<div className="mb-1">
<Typography variant="h6">Transfer project</Typography>
<Typography variant="small">
Transfer this app to your personal account or a team you are a member
of.{' '}
<Link to="" className="text-blue-500">
Learn more
</Link>
</Typography>
<form
onSubmit={handleTransfer(({ orgId }) => {
setSelectedTransferOrganization(orgId);
onSubmit={handleTransfer((org) => {
setSelectedTransferOrganization(org.org);
setOpenTransferDialog(!openTransferDialog);
})}
className="self-stretch space-y-3 px-2"
>
<Typography variant="small" className="font-medium text-gray-800">
Choose team
</Typography>
<Controller
name="orgId"
rules={{ required: 'This field is required' }}
control={control}
render={({ field }) => (
<AsyncSelect
{...field}
// TODO: Implement placeholder for select
label={!field.value ? 'Select an account / team' : ''}
>
{transferOrganizations.map((org, key) => (
<Option key={key} value={org.id}>
^ {org.name}
</Option>
))}
</AsyncSelect>
)}
<Heading className="text-sky-950 text-lg font-medium leading-normal">
Transfer project
</Heading>
<p className="text-slate-600 text-sm font-normal leading-tight">
Transfer this app to your personal account or a team you are a member
of.
</p>
<Select
disabled
size="md"
placeholder="Select an account / team"
options={transferOrganizations}
value={selectedTransferOrganization}
onChange={(value) =>
setSelectedTransferOrganization(value as SelectOption)
}
/>
<Button
variant="gradient"
size="sm"
className="mt-1"
disabled={!formState.isValid}
type="submit"
>
<Button disabled type="submit" size="md">
Transfer
</Button>
</form>
@ -215,20 +199,21 @@ const GeneralTabPanel = () => {
from={project.organization.name}
to={selectedUserOrgName}
/>
</div>
<div className="mb-1">
<Typography variant="h6">Delete project</Typography>
<Typography variant="small">
<div className="self-stretch space-y-3 px-2">
<Heading className="text-sky-950 text-lg font-medium leading-normal">
Delete project
</Heading>
<p className="text-slate-600 text-sm font-normal leading-tight">
The project will be permanently deleted, including its deployments and
domains. This action is irreversible and can not be undone.
</Typography>
</p>
<Button
variant="gradient"
size="sm"
color="red"
size="md"
variant="danger"
onClick={handleDeleteProjectDialog}
leftIcon={<TrashIcon />}
>
^ Delete project
Delete project
</Button>
<DeleteProjectDialog
handleOpen={handleDeleteProjectDialog}
@ -236,7 +221,7 @@ const GeneralTabPanel = () => {
project={project}
/>
</div>
</>
</div>
);
};

View File

@ -0,0 +1,3 @@
export * from './common';
export * from './types';
export * from './vendor';