forked from cerc-io/snowballtools-base
[1/n][project settings ui] GeneralTabPanel (#23)
This commit is contained in:
commit
57956ec269
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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';
|
||||
|
@ -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,110 +120,100 @@ 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"
|
||||
value={project.id}
|
||||
size="md"
|
||||
disabled
|
||||
icon={<CopyIcon value={project.id} />}
|
||||
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
|
||||
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">
|
||||
<form
|
||||
onSubmit={handleTransfer((org) => {
|
||||
setSelectedTransferOrganization(org.org);
|
||||
setOpenTransferDialog(!openTransferDialog);
|
||||
})}
|
||||
className="self-stretch space-y-3 px-2"
|
||||
>
|
||||
<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.{' '}
|
||||
<Link to="" className="text-blue-500">
|
||||
Learn more
|
||||
</Link>
|
||||
</Typography>
|
||||
<form
|
||||
onSubmit={handleTransfer(({ orgId }) => {
|
||||
setSelectedTransferOrganization(orgId);
|
||||
setOpenTransferDialog(!openTransferDialog);
|
||||
})}
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
variant="gradient"
|
||||
size="sm"
|
||||
className="mt-1"
|
||||
disabled={!formState.isValid}
|
||||
type="submit"
|
||||
>
|
||||
Transfer
|
||||
</Button>
|
||||
</form>
|
||||
<TransferProjectDialog
|
||||
handleCancel={() => setOpenTransferDialog(!openTransferDialog)}
|
||||
open={openTransferDialog}
|
||||
handleConfirm={handleTransferProject}
|
||||
projectName={project.name}
|
||||
from={project.organization.name}
|
||||
to={selectedUserOrgName}
|
||||
of.
|
||||
</p>
|
||||
<Select
|
||||
disabled
|
||||
size="md"
|
||||
placeholder="Select an account / team"
|
||||
options={transferOrganizations}
|
||||
value={selectedTransferOrganization}
|
||||
onChange={(value) =>
|
||||
setSelectedTransferOrganization(value as SelectOption)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-1">
|
||||
<Typography variant="h6">Delete project</Typography>
|
||||
<Typography variant="small">
|
||||
<Button disabled type="submit" size="md">
|
||||
Transfer
|
||||
</Button>
|
||||
</form>
|
||||
<TransferProjectDialog
|
||||
handleCancel={() => setOpenTransferDialog(!openTransferDialog)}
|
||||
open={openTransferDialog}
|
||||
handleConfirm={handleTransferProject}
|
||||
projectName={project.name}
|
||||
from={project.organization.name}
|
||||
to={selectedUserOrgName}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
|
3
packages/frontend/src/types/index.ts
Normal file
3
packages/frontend/src/types/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './common';
|
||||
export * from './types';
|
||||
export * from './vendor';
|
Loading…
Reference in New Issue
Block a user