mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-21 23:19:09 +00:00
feat: allow renaming rundown
This commit is contained in:
committed by
Carlos Valente
parent
812b17a221
commit
74e59dccdb
@@ -45,6 +45,13 @@ export async function duplicateRundown(rundownId: RundownId): Promise<AxiosRespo
|
|||||||
return axios.post(`${rundownPath}/${rundownId}/duplicate`);
|
return axios.post(`${rundownPath}/${rundownId}/duplicate`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to rename an existing rundown
|
||||||
|
*/
|
||||||
|
export async function renameRundown(rundownId: RundownId, title: string): Promise<AxiosResponse<ProjectRundownsList>> {
|
||||||
|
return axios.patch(`${rundownPath}/${rundownId}`, { title });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to delete a rundown
|
* HTTP request to delete a rundown
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ProjectRundownsList } from 'ontime-types';
|
|||||||
|
|
||||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||||
import { PROJECT_RUNDOWNS } from '../api/constants';
|
import { PROJECT_RUNDOWNS } from '../api/constants';
|
||||||
import { createRundown, deleteRundown, duplicateRundown, fetchProjectRundownList, loadRundown } from '../api/rundown';
|
import { createRundown, deleteRundown, duplicateRundown, fetchProjectRundownList, loadRundown, renameRundown } from '../api/rundown';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Project rundowns
|
* Project rundowns
|
||||||
@@ -40,6 +40,16 @@ export function useMutateProjectRundowns() {
|
|||||||
ontimeQueryClient.setQueryData(PROJECT_RUNDOWNS, response.data);
|
ontimeQueryClient.setQueryData(PROJECT_RUNDOWNS, response.data);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: rename } = useMutation({
|
||||||
|
mutationFn: ([rundownId, title]: Parameters<typeof renameRundown>) => renameRundown(rundownId, title),
|
||||||
|
onMutate: () => {
|
||||||
|
ontimeQueryClient.cancelQueries({ queryKey: PROJECT_RUNDOWNS });
|
||||||
|
},
|
||||||
|
onSuccess: (response) => {
|
||||||
|
ontimeQueryClient.setQueryData(PROJECT_RUNDOWNS, response.data);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const { mutateAsync: remove } = useMutation({
|
const { mutateAsync: remove } = useMutation({
|
||||||
mutationFn: deleteRundown,
|
mutationFn: deleteRundown,
|
||||||
@@ -61,5 +71,5 @@ export function useMutateProjectRundowns() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return { create, duplicate, remove, load };
|
return { create, duplicate, remove, load, rename };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { IoAdd, IoDocumentOutline, IoDownloadOutline, IoDuplicateOutline, IoEllipsisHorizontal, IoTrash } from 'react-icons/io5';
|
import {
|
||||||
|
IoAdd,
|
||||||
|
IoDocumentOutline,
|
||||||
|
IoDownloadOutline,
|
||||||
|
IoDuplicateOutline,
|
||||||
|
IoEllipsisHorizontal,
|
||||||
|
IoPencilOutline,
|
||||||
|
IoTrash,
|
||||||
|
} from 'react-icons/io5';
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
|
|
||||||
import { downloadAsExcel } from '../../../../common/api/excel';
|
import { downloadAsExcel } from '../../../../common/api/excel';
|
||||||
@@ -13,17 +21,19 @@ import { useMutateProjectRundowns, useProjectRundowns } from '../../../../common
|
|||||||
import { cx } from '../../../../common/utils/styleUtils';
|
import { cx } from '../../../../common/utils/styleUtils';
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
|
import RundownRenameForm from './composite/RundownRenameForm';
|
||||||
import { ManageRundownForm } from './ManageRundownForm';
|
import { ManageRundownForm } from './ManageRundownForm';
|
||||||
|
|
||||||
import style from './ManagePanel.module.scss';
|
import style from './ManagePanel.module.scss';
|
||||||
|
|
||||||
export default function ManageRundowns() {
|
export default function ManageRundowns() {
|
||||||
const { data } = useProjectRundowns();
|
const { data } = useProjectRundowns();
|
||||||
const { duplicate, remove, load } = useMutateProjectRundowns();
|
const { duplicate, remove, load, rename } = useMutateProjectRundowns();
|
||||||
const [isOpenDelete, deleteHandlers] = useDisclosure();
|
const [isOpenDelete, deleteHandlers] = useDisclosure();
|
||||||
const [isOpenLoad, loadHandlers] = useDisclosure();
|
const [isOpenLoad, loadHandlers] = useDisclosure();
|
||||||
const [isNewLoad, newHandlers] = useDisclosure();
|
const [isNewLoad, newHandlers] = useDisclosure();
|
||||||
const [targetRundown, setTargetRundown] = useState('');
|
const [targetRundown, setTargetRundown] = useState('');
|
||||||
|
const [renamingRundown, setRenamingRundown] = useState<string | null>(null);
|
||||||
const [actionError, setActionError] = useState<string | null>(null);
|
const [actionError, setActionError] = useState<string | null>(null);
|
||||||
|
|
||||||
const openLoad = (id: string) => {
|
const openLoad = (id: string) => {
|
||||||
@@ -38,6 +48,11 @@ export default function ManageRundowns() {
|
|||||||
deleteHandlers.open();
|
deleteHandlers.open();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openRename = (id: string) => {
|
||||||
|
setActionError(null);
|
||||||
|
setRenamingRundown(id);
|
||||||
|
};
|
||||||
|
|
||||||
const submitRundownLoad = async () => {
|
const submitRundownLoad = async () => {
|
||||||
try {
|
try {
|
||||||
await load(targetRundown);
|
await load(targetRundown);
|
||||||
@@ -60,6 +75,15 @@ export default function ManageRundowns() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const submitRundownRename = async (id: string, newTitle: string) => {
|
||||||
|
try {
|
||||||
|
await rename([id, newTitle]);
|
||||||
|
setRenamingRundown(null);
|
||||||
|
} catch (error) {
|
||||||
|
setActionError(`Failed to rename rundown. ${maybeAxiosError(error)}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const submitRundownDelete = async () => {
|
const submitRundownDelete = async () => {
|
||||||
try {
|
try {
|
||||||
await remove(targetRundown);
|
await remove(targetRundown);
|
||||||
@@ -106,6 +130,22 @@ export default function ManageRundowns() {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{data?.rundowns?.map(({ id, numEntries, title }) => {
|
{data?.rundowns?.map(({ id, numEntries, title }) => {
|
||||||
const isLoaded = data.loaded === id;
|
const isLoaded = data.loaded === id;
|
||||||
|
const isRenaming = renamingRundown === id;
|
||||||
|
|
||||||
|
if (isRenaming) {
|
||||||
|
return (
|
||||||
|
<tr key={id}>
|
||||||
|
<td colSpan={3}>
|
||||||
|
<RundownRenameForm
|
||||||
|
onCancel={() => setRenamingRundown(null)}
|
||||||
|
onSubmit={(newTitle: string) => submitRundownRename(id, newTitle)}
|
||||||
|
initialTitle={title}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={id} className={cx([isLoaded && style.current])}>
|
<tr key={id} className={cx([isLoaded && style.current])}>
|
||||||
<td>{numEntries}</td>
|
<td>{numEntries}</td>
|
||||||
@@ -116,6 +156,12 @@ export default function ManageRundowns() {
|
|||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
render={<IconButton variant='ghosted-white' />}
|
render={<IconButton variant='ghosted-white' />}
|
||||||
items={[
|
items={[
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
icon: IoPencilOutline,
|
||||||
|
label: 'Rename',
|
||||||
|
onClick: () => openRename(id),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'item',
|
type: 'item',
|
||||||
icon: IoDownloadOutline,
|
icon: IoDownloadOutline,
|
||||||
@@ -137,7 +183,7 @@ export default function ManageRundowns() {
|
|||||||
},
|
},
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
type: 'item',
|
type: 'destructive',
|
||||||
icon: IoTrash,
|
icon: IoTrash,
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
onClick: () => openDelete(id),
|
onClick: () => openDelete(id),
|
||||||
|
|||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { checkRegex } from 'ontime-utils';
|
||||||
|
|
||||||
|
import { maybeAxiosError } from '../../../../../common/api/utils';
|
||||||
|
import Button from '../../../../../common/components/buttons/Button';
|
||||||
|
import Input from '../../../../../common/components/input/input/Input';
|
||||||
|
import { preventEscape } from '../../../../../common/utils/keyEvent';
|
||||||
|
import * as Panel from '../../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
|
interface RundownRenameFormProps {
|
||||||
|
onSubmit: (newTitle: string) => Promise<void>;
|
||||||
|
onCancel: () => void;
|
||||||
|
initialTitle: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormData {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RundownRenameForm({ onSubmit, onCancel, initialTitle }: RundownRenameFormProps) {
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
register,
|
||||||
|
setFocus,
|
||||||
|
setError,
|
||||||
|
formState: { errors, isSubmitting, isValid, isDirty },
|
||||||
|
} = useForm<FormData>({
|
||||||
|
defaultValues: { title: initialTitle },
|
||||||
|
mode: 'onChange',
|
||||||
|
});
|
||||||
|
|
||||||
|
const setupSubmit = async (values: FormData) => {
|
||||||
|
try {
|
||||||
|
await onSubmit(values.title);
|
||||||
|
} catch (error) {
|
||||||
|
setError('root', { type: 'custom', message: maybeAxiosError(error) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Give initial focus to the title input
|
||||||
|
useEffect(() => {
|
||||||
|
setFocus('title');
|
||||||
|
}, [setFocus]);
|
||||||
|
|
||||||
|
const canSubmit = isDirty && isValid;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel.Indent as='form' onSubmit={handleSubmit(setupSubmit)} onKeyDown={(event) => preventEscape(event, onCancel)}>
|
||||||
|
<label>
|
||||||
|
<Panel.Description>Rundown title</Panel.Description>
|
||||||
|
<Input
|
||||||
|
{...register('title', {
|
||||||
|
required: { value: true, message: 'Title is required' },
|
||||||
|
validate: (value) => {
|
||||||
|
if (value.trim().length === 0) return 'Title cannot be empty';
|
||||||
|
if (checkRegex.isAlphanumericWithSpace(value) === false)
|
||||||
|
return 'Title can only contain alphanumeric characters, spaces and underscores';
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
fluid
|
||||||
|
/>
|
||||||
|
{errors.title && <Panel.Error>{errors.title.message}</Panel.Error>}
|
||||||
|
</label>
|
||||||
|
{errors.root && <Panel.Error>{errors.root.message}</Panel.Error>}
|
||||||
|
<Panel.InlineElements relation='inner' align='end'>
|
||||||
|
<Button variant='ghosted' onClick={onCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type='submit' variant='primary' disabled={!canSubmit} loading={isSubmitting}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Panel.InlineElements>
|
||||||
|
</Panel.Indent>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -113,6 +113,38 @@ router.post('/:id/duplicate', paramsWithId, async (req: Request, res: Response<P
|
|||||||
res.status(400).send({ message });
|
res.status(400).send({ message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patches the data of an existing rundown
|
||||||
|
* Currently only the title can be changed
|
||||||
|
*/
|
||||||
|
router.patch('/:id', paramsWithId, async (req: Request, res: Response<ProjectRundownsList | ErrorResponse>) => {
|
||||||
|
try {
|
||||||
|
const dataProvider = getDataProvider();
|
||||||
|
const rundown = dataProvider.getRundown(req.params.id);
|
||||||
|
if (!rundown) throw new Error(`Rundown with ID ${req.params.id} not found`);
|
||||||
|
if (!req.body.title) throw new Error('No title provided');
|
||||||
|
|
||||||
|
await dataProvider.setRundown(rundown.id, { ...rundown, title: req.body.title });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If loaded we re-init the rundown
|
||||||
|
* This is likely over-kill but the simplest way to ensure state consistency
|
||||||
|
*/
|
||||||
|
if (req.params.id === getCurrentRundown().id) {
|
||||||
|
const rundown = dataProvider.getRundown(req.params.id);
|
||||||
|
const customField = dataProvider.getCustomFields();
|
||||||
|
await initRundown(rundown, customField);
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectRundowns = getDataProvider().getProjectRundowns();
|
||||||
|
res.status(201).json({ loaded: getCurrentRundown().id, rundowns: normalisedToRundownArray(projectRundowns) });
|
||||||
|
} catch (error) {
|
||||||
|
const message = getErrorMessage(error);
|
||||||
|
res.status(400).send({ message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a rundown if not loaded
|
* Deletes a rundown if not loaded
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user