mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-01 20:39:18 +00:00
refactor: migrate project upload (#796)
* refactor: migrate project upload
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user