feat: custom data for projects (#1571)

This commit is contained in:
Shobhit Nagpal
2025-05-17 18:32:50 +05:30
committed by Carlos Valente
parent eed6373dbf
commit 696c016c90
24 changed files with 264 additions and 33 deletions
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useFieldArray, useForm } from 'react-hook-form';
import { IoTrash } from 'react-icons/io5';
import { Button, Input, Textarea } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
@@ -23,6 +24,7 @@ type ProjectCreateFormValues = {
publicUrl?: string;
backstageInfo?: string;
backstageUrl?: string;
custom?: { title: string; value: string }[];
};
export default function ProjectCreateForm(props: ProjectCreateFromProps) {
@@ -34,6 +36,7 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
const {
handleSubmit,
register,
control,
formState: { isSubmitting, isValid },
setFocus,
} = useForm<ProjectCreateFormValues>({
@@ -44,6 +47,11 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
},
});
const { fields, append, remove } = useFieldArray({
control,
name: 'custom',
});
// set focus to first field
useEffect(() => {
setFocus('title');
@@ -59,6 +67,7 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
...values,
filename,
});
await queryClient.invalidateQueries({ queryKey: PROJECT_LIST });
onClose();
} catch (error) {
@@ -66,6 +75,10 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
}
};
const handleAddCustom = () => {
append({ title: '', value: '' });
};
return (
<Panel.Section
as='form'
@@ -151,6 +164,42 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
{...register('backstageUrl')}
/>
</label>
<Panel.Section>
<Panel.ListItem>
<Panel.Field title='Custom data' description='Add custom data for your project' />
<Button variant='ontime-subtle' onClick={handleAddCustom}>
+
</Button>
</Panel.ListItem>
{fields.map((field, idx) => (
<div key={field.id} className={style.customDataItem}>
<Panel.Paragraph>{idx + 1}.</Panel.Paragraph>
<label>
Title
<Input
variant='ontime-filled'
size='sm'
placeholder={field.title}
autoComplete='off'
{...register(`custom.${idx}.title` as const)}
/>
</label>
<label>
Value
<Input
variant='ontime-filled'
size='sm'
placeholder={field.value}
autoComplete='off'
{...register(`custom.${idx}.value` as const)}
/>
</label>
<Button variant='ontime-ghosted' onClick={() => remove(idx)}>
<IoTrash />
</Button>
</div>
))}
</Panel.Section>
</Panel.Section>
</Panel.Section>
);
@@ -1,6 +1,6 @@
import { ChangeEvent, useEffect, useRef } from 'react';
import { useForm } from 'react-hook-form';
import { IoDownloadOutline, IoTrash } from 'react-icons/io5';
import { useFieldArray, useForm } from 'react-hook-form';
import { IoAdd, IoDownloadOutline, IoTrash } from 'react-icons/io5';
import { Button, Input, Textarea } from '@chakra-ui/react';
import { type ProjectData } from 'ontime-types';
@@ -25,6 +25,7 @@ export default function ProjectData() {
formState: { isSubmitting, isValid, isDirty, errors },
setError,
watch,
control,
setValue,
} = useForm({
defaultValues: data,
@@ -32,6 +33,12 @@ export default function ProjectData() {
resetOptions: {
keepDirtyValues: true,
},
mode: 'onChange',
});
const { fields, append, remove } = useFieldArray({
control,
name: 'custom',
});
// reset form values if data changes
@@ -77,6 +84,10 @@ export default function ProjectData() {
});
};
const handleAddCustom = () => {
append({ title: '', value: '' });
};
const onSubmit = async (formData: ProjectData) => {
try {
await postProjectData(formData);
@@ -231,6 +242,69 @@ export default function ProjectData() {
{...register('backstageUrl')}
/>
</label>
<Panel.Section style={{ marginTop: 0 }}>
<Panel.ListItem>
<Panel.Field title='Custom data' description='' />
<Button leftIcon={<IoAdd />} size='sm' variant='ontime-subtle' onClick={handleAddCustom}>
Add
</Button>
</Panel.ListItem>
{fields.length > 0 &&
fields.map((field, idx) => {
const rowErrors = errors.custom?.[idx] as
| {
title?: { message?: string };
value?: { message?: string };
}
| undefined;
return (
<div key={field.id} className={style.customDataItem}>
<div>
<div className={style.titleRow}>
<label>
Title
<Input
variant='ontime-filled'
size='sm'
defaultValue={field.title}
placeholder='Title of your custom data'
autoComplete='off'
{...register(`custom.${idx}.title`, {
required: { value: true, message: 'Field cannot be empty' },
})}
/>
</label>
<Button
size='sm'
variant='ontime-subtle'
color='#FA5656' // $red-500
onClick={() => remove(idx)}
leftIcon={<IoTrash />}
>
Delete Entry
</Button>
</div>
{rowErrors?.title?.message && <Panel.Error>{rowErrors.title.message}</Panel.Error>}
</div>
<label>
Value
<Textarea
variant='ontime-filled'
resize='none'
size='sm'
defaultValue={field.value}
autoComplete='off'
placeholder='Text of your custom data'
{...register(`custom.${idx}.value`, {
required: { value: true, message: 'Field cannot be empty' },
})}
/>
{rowErrors?.value?.message && <Panel.Error>{rowErrors.value.message}</Panel.Error>}
</label>
</div>
);
})}
</Panel.Section>
</Panel.Section>
</Panel.Card>
</Panel.Section>
@@ -57,3 +57,18 @@
height: auto;
}
}
.customDataItem {
display: contents;
width: 100%;
.titleRow{
display: flex;
gap: 1rem;
align-items: end;
label {
flex: 1;
}
}
}