refactor: migrate project upload (#796)

* refactor: migrate project upload
This commit is contained in:
Carlos Valente
2024-03-01 22:59:00 +01:00
committed by GitHub
parent 8df419d835
commit 5b01329c37
54 changed files with 698 additions and 1134 deletions
@@ -1,10 +1,18 @@
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { Button, Input, Textarea } from '@chakra-ui/react';
import { maybeAxiosError } from '../../../../common/api/apiUtils';
import { createProject } from '../../../../common/api/ontimeApi';
import * as Panel from '../PanelUtils';
import style from './ProjectPanel.module.scss';
export type ProjectCreateFormValues = {
interface ProjectCreateFromProps {
onClose: () => void;
}
type ProjectCreateFormValues = {
title?: string;
description?: string;
publicInfo?: string;
@@ -13,13 +21,11 @@ export type ProjectCreateFormValues = {
backstageUrl?: string;
};
interface ProjectCreateFormProps {
onCancel: () => void;
onSubmit: (values: ProjectCreateFormValues) => Promise<void>;
submitError: string | null;
}
export default function ProjectCreateForm(props: ProjectCreateFromProps) {
const { onClose } = props;
const [error, setError] = useState<string | null>(null);
export default function ProjectCreateForm({ onSubmit, onCancel, submitError }: ProjectCreateFormProps) {
const {
handleSubmit,
register,
@@ -33,12 +39,40 @@ export default function ProjectCreateForm({ onSubmit, onCancel, submitError }: P
},
});
// set focus to first field
useEffect(() => {
setFocus('title');
}, [setFocus]);
const handleSubmitCreate = async (values: ProjectCreateFormValues) => {
try {
setError(null);
const filename = values.title?.trim();
await createProject({
...values,
filename,
});
onClose();
} catch (error) {
setError(maybeAxiosError(error));
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Panel.Section as='form' onSubmit={handleSubmit(handleSubmitCreate)}>
<Panel.Title>
Create new project
<div className={style.createActionButtons}>
<Button onClick={onClose} variant='ontime-ghosted' size='sm' isDisabled={isSubmitting}>
Cancel
</Button>
<Button isDisabled={!isValid} type='submit' isLoading={isSubmitting} variant='ontime-filled' size='sm'>
Create
</Button>
</div>
</Panel.Title>
{error && <Panel.Error>{error}</Panel.Error>}
<div className={style.createFormInputField}>
<label>
Project title
@@ -115,22 +149,6 @@ export default function ProjectCreateForm({ onSubmit, onCancel, submitError }: P
/>
</label>
</div>
<div className={style.createActionButtons}>
<Button onClick={onCancel} variant='ontime-ghosted' size='sm'>
Cancel
</Button>
<Button
isDisabled={!isValid}
type='submit'
isLoading={isSubmitting}
variant='ontime-filled'
padding='0 2em'
size='sm'
>
Create
</Button>
</div>
{submitError && <span className={style.error}>{submitError}</span>}
</form>
</Panel.Section>
);
}
@@ -2,6 +2,8 @@ import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { Button, Input } from '@chakra-ui/react';
import * as Panel from '../PanelUtils';
import style from './ProjectPanel.module.scss';
export type ProjectFormValues = {
@@ -62,7 +64,7 @@ export default function ProjectForm({ action, filename, onSubmit, onCancel, subm
</Button>
</div>
</form>
{submitError && <span className={style.error}>{submitError}</span>}
{submitError && <Panel.Error>{submitError}</Panel.Error>}
</>
);
}
@@ -1,48 +1,18 @@
import { useMemo, useState } from 'react';
import { maybeAxiosError } from '../../../../common/api/apiUtils';
import { createProject } from '../../../../common/api/ontimeApi';
import { useProjectList } from '../../../../common/hooks-query/useProjectList';
import * as Panel from '../PanelUtils';
import ProjectCreateForm, { ProjectCreateFormValues } from './ProjectCreateForm';
import ProjectListItem, { EditMode } from './ProjectListItem';
import style from './ProjectPanel.module.scss';
interface ProjectListProps {
isCreatingProject: boolean;
onToggleCreate: () => void;
}
export default function ProjectList({ isCreatingProject, onToggleCreate }: ProjectListProps) {
export default function ProjectList() {
const { data, refetch } = useProjectList();
const { files, lastLoadedProject } = data;
const [editingMode, setEditingMode] = useState<EditMode | null>(null);
const [editingFilename, setEditingFilename] = useState<string | null>(null);
const [submitError, setSubmitError] = useState<string | null>(null);
const handleToggleCreateMode = () => {
onToggleCreate();
setSubmitError(null);
};
const handleSubmitCreate = async (values: ProjectCreateFormValues) => {
try {
setSubmitError(null);
const filename = values.title?.trim();
await createProject({
...values,
filename,
});
await refetch();
handleToggleCreateMode();
} catch (error) {
setSubmitError(maybeAxiosError(error));
}
};
const handleToggleEditMode = (editMode: EditMode, filename: string | null) => {
setEditingMode((prev) => (prev === editMode && filename === editingFilename ? null : editMode));
@@ -79,14 +49,6 @@ export default function ProjectList({ isCreatingProject, onToggleCreate }: Proje
</tr>
</thead>
<tbody>
{isCreatingProject ? (
<tr className={style.createContainer}>
<td colSpan={99}>
<ProjectCreateForm onSubmit={handleSubmitCreate} onCancel={handleToggleCreateMode} submitError='' />
{submitError && <span className={style.createSubmitError}>{submitError}</span>}
</td>
</tr>
) : null}
{reorderedProjectFiles.map((project) => (
<ProjectListItem
key={project.filename}
@@ -85,9 +85,10 @@ export default function ProjectListItem({
};
const isCurrentlyBeingEdited = editingMode && filename === editingFilename;
const classes = current && !isCurrentlyBeingEdited ? style.current : undefined;
return (
<tr key={filename} className={current ? style.current : undefined}>
<tr key={filename} className={classes}>
{isCurrentlyBeingEdited ? (
<td colSpan={99}>
<ProjectForm
@@ -160,6 +161,7 @@ function ActionMenu({
as={IconButton}
aria-label='Options'
icon={<IoEllipsisHorizontal />}
color='#e2e2e2' // $gray-200
variant='ontime-ghosted'
size='sm'
/>
@@ -1,6 +1,5 @@
.current {
color: $blue-400;
background-color: $gray-1350;
background-color: $blue-900;
}
.actionButton {
@@ -23,25 +22,23 @@
padding-bottom: 0.5rem;
}
.actionButtons {
.headerButtons,
.actionButtons,
.createActionButtons {
display: flex;
align-items: center;
gap: 0.5rem;
gap: 1rem;
}
.actionButtons {
margin-left: 1rem;
}
.createActionButtons {
display: flex;
align-items: center;
gap: 0.5rem;
margin-left: 1rem;
justify-content: flex-end;
}
.error {
color: $red-500;
}
.createContainer {
padding: 0.5rem 0;
}
@@ -50,12 +47,6 @@
text-transform: capitalize;
}
.createSubmitError {
font-size: calc(1rem - 2px);
font-weight: 400;
color: $red-500;
}
.containCell {
max-width: 400px;
}
@@ -1,34 +1,92 @@
import { useState } from 'react';
import { Button } from '@chakra-ui/react';
import { ChangeEvent, useRef, useState } from 'react';
import { Button, Input } from '@chakra-ui/react';
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/apiUtils';
import { importProjectFile } from '../../../../common/api/ontimeApi';
import { validateProjectFile } from '../../../../common/utils/uploadUtils';
import * as Panel from '../PanelUtils';
import ProjectCreateForm from './ProjectCreateForm';
import ProjectList from './ProjectList';
import style from './ProjectPanel.module.scss';
export default function ProjectPanel() {
const [isCreatingProject, setIsCreatingProject] = useState(false);
const [error, setError] = useState('');
const [loading, setLoading] = useState<'import' | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleToggleCreate = () => {
setIsCreatingProject((prev) => !prev);
};
const handleSelectFile = () => {
fileInputRef.current?.click();
};
const handleImport = async (event: ChangeEvent<HTMLInputElement>) => {
const selectedFile = event.target?.files?.[0];
if (!selectedFile) {
return;
}
setLoading('import');
try {
validateProjectFile(selectedFile);
await importProjectFile(selectedFile);
} catch (error) {
const errorMessage = maybeAxiosError(error);
setError(`Error uploading file: ${errorMessage}`);
} finally {
invalidateAllCaches();
}
setLoading(null);
};
return (
<>
<Panel.Header>Project</Panel.Header>
<Input
ref={fileInputRef}
style={{ display: 'none' }}
type='file'
onChange={handleImport}
accept='.json'
data-testid='file-input'
/>
<Panel.Section>
<Panel.Card>
<Panel.SubHeader>
Manage projects
<div style={{ display: 'flex', gap: '1rem' }}>
<Button variant='ontime-subtle' onClick={handleToggleCreate} size='sm'>
<div className={style.headerButtons}>
<Button
variant='ontime-subtle'
onClick={handleSelectFile}
size='sm'
isDisabled={Boolean(loading) || isCreatingProject}
isLoading={loading === 'import'}
>
Import
</Button>
<Button variant='ontime-subtle' onClick={handleToggleCreate} size='sm'>
<Button
variant='ontime-subtle'
onClick={handleToggleCreate}
size='sm'
isDisabled={Boolean(loading) || isCreatingProject}
rightIcon={<IoAdd />}
>
Add
</Button>
</div>
</Panel.SubHeader>
<ProjectList onToggleCreate={handleToggleCreate} isCreatingProject={isCreatingProject} />
{error && <Panel.Error>{error}</Panel.Error>}
{isCreatingProject && <ProjectCreateForm onClose={() => setIsCreatingProject(false)} />}
<ProjectList />
</Panel.Card>
</Panel.Section>
</>