mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-16 04:43:35 +00:00
feat: auto cue re-numbering (#2016)
* feat: update auto cue numbering * feat: renumber from ui * refactor: patchEntries is not used * chore: format * fix: correct cue at top of group * fix: handle precision * refactor dialog * bump limit for performance time test * extract type * add class name to lable * fix rebase * refator: extract renumering logic * chore: comments for getIntegerAndFraction function * chore: add the for renumber mutation * fix: fraction match precision * refactor: small cleanup * refactor: use more narrow validator --------- Co-authored-by: alex-Arc <omnivox@LAPTOP-RC5SNBVV.localdomain>
This commit is contained in:
committed by
GitHub
parent
ba1c3235b3
commit
1a08b39b8b
@@ -1,5 +1,13 @@
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { EntryId, OntimeEntry, OntimeEvent, ProjectRundownsList, Rundown, TransientEventPayload } from 'ontime-types';
|
||||
import {
|
||||
EntryId,
|
||||
OntimeEntry,
|
||||
OntimeEvent,
|
||||
ProjectRundownsList,
|
||||
RenumberCues,
|
||||
Rundown,
|
||||
TransientEventPayload,
|
||||
} from 'ontime-types';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
import type { RequestOptions } from './requestOptions';
|
||||
@@ -111,6 +119,13 @@ export async function putBatchEditEvents(rundownId: RundownId, data: BatchEditEn
|
||||
return axios.put(`${rundownPath}/${rundownId}/batch`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to renumber cues for multiple events
|
||||
*/
|
||||
export function patchRenumberCues(rundownId: RundownId, data: RenumberCues): Promise<AxiosResponse<Rundown>> {
|
||||
return axios.patch(`${rundownPath}/${rundownId}/renumber`, data);
|
||||
}
|
||||
|
||||
export type ReorderEntry = {
|
||||
entryId: EntryId;
|
||||
destinationId: EntryId;
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
patchReorderEntry,
|
||||
postAddEntry,
|
||||
postCloneEntry,
|
||||
patchRenumberCues,
|
||||
putBatchEditEvents,
|
||||
putEditEntry,
|
||||
requestApplyDelay,
|
||||
@@ -523,6 +524,39 @@ export const useEntryActions = () => {
|
||||
[batchUpdateEventsMutation, getCurrentRundownData],
|
||||
);
|
||||
|
||||
const { mutateAsync: renumberCuesMutation } = useMutation({
|
||||
mutationFn: ([rundownId, body]: Parameters<typeof patchRenumberCues>) => patchRenumberCues(rundownId, body),
|
||||
onMutate: async () => {
|
||||
const queryKey = resolveCurrentRundownQueryKey();
|
||||
await queryClient.cancelQueries({ queryKey });
|
||||
const previousRundown = queryClient.getQueryData<Rundown>(queryKey);
|
||||
return { previousRundown, queryKey };
|
||||
},
|
||||
onSuccess: (response, _variables, context) => {
|
||||
if (!response.data || !context?.queryKey) return;
|
||||
const updatedRundown = response.data;
|
||||
queryClient.setQueryData<Rundown>(context.queryKey, updatedRundown);
|
||||
},
|
||||
onError: (_error, _vars, context) => {
|
||||
if (context?.previousRundown) queryClient.setQueryData<Rundown>(context.queryKey, context.previousRundown);
|
||||
},
|
||||
});
|
||||
|
||||
const renumberCues = useCallback(
|
||||
async (eventIds: EntryId[], prefix: string, start: string, increment: string) => {
|
||||
const rundown = getCurrentRundownData();
|
||||
const rundownId = rundown?.id;
|
||||
if (!rundownId) throw new Error('Rundown not initialized');
|
||||
try {
|
||||
await renumberCuesMutation([rundownId, { ids: eventIds, prefix, start, increment }]);
|
||||
} catch (error) {
|
||||
logAxiosError('Error renumbering cues', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[getCurrentRundownData, renumberCuesMutation],
|
||||
);
|
||||
|
||||
/**
|
||||
* Calls mutation to delete an entry
|
||||
* @private
|
||||
@@ -947,6 +981,7 @@ export const useEntryActions = () => {
|
||||
groupEntries,
|
||||
move,
|
||||
reorderEntry,
|
||||
renumberCues,
|
||||
swapEvents,
|
||||
updateEntry,
|
||||
updateTimer,
|
||||
@@ -963,6 +998,7 @@ export const useEntryActions = () => {
|
||||
groupEntries,
|
||||
move,
|
||||
reorderEntry,
|
||||
renumberCues,
|
||||
swapEvents,
|
||||
updateEntry,
|
||||
updateTimer,
|
||||
|
||||
@@ -16,6 +16,7 @@ import EntryEditModal from '../../views/cuesheet/cuesheet-edit-modal/EntryEditMo
|
||||
import { EditorLayoutMode, useEditorLayout } from '../../views/editor/useEditorLayout';
|
||||
import RundownEntryEditor from './entry-editor/RundownEntryEditor';
|
||||
import FinderPlacement from './placements/FinderPlacement';
|
||||
import RenumberCuesDialog from './renumber-cues-dialog/RenumberCuesDialog';
|
||||
import { RundownContextMenu } from './rundown-context-menu/RundownContextMenu';
|
||||
import RundownHeader from './rundown-header/RundownHeader';
|
||||
import RundownHeaderMobile from './rundown-header/RundownHeaderMobile';
|
||||
@@ -112,6 +113,7 @@ function RundownRoot({ isSmallDevice, isExtracted, viewMode, setViewMode }: Rund
|
||||
)}
|
||||
{viewMode === RundownViewMode.List ? <RundownList /> : <RundownTable />}
|
||||
{viewMode === RundownViewMode.Table && <EntryEditModal />}
|
||||
<RenumberCuesDialog />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { sanitiseCue } from 'ontime-utils';
|
||||
import { memo } from 'react';
|
||||
|
||||
import * as Editor from '../../../../common/components/editor-utils/EditorUtils';
|
||||
@@ -24,10 +23,6 @@ export default memo(EventEditorTitles);
|
||||
function EventEditorTitles({ eventId, cue, flag, title, note, colour }: EventEditorTitlesProps) {
|
||||
const { updateEntry } = useEntryActionsContext();
|
||||
|
||||
const cueSubmitHandler = (_field: string, newValue: string) => {
|
||||
updateEntry({ id: eventId, cue: sanitiseCue(newValue) });
|
||||
};
|
||||
|
||||
const flagSubmitHandler = (newValue: boolean) => {
|
||||
updateEntry({ id: eventId, flag: newValue });
|
||||
};
|
||||
@@ -48,7 +43,7 @@ function EventEditorTitles({ eventId, cue, flag, title, note, colour }: EventEdi
|
||||
field='cue'
|
||||
label='Cue'
|
||||
initialValue={cue}
|
||||
submitHandler={cueSubmitHandler}
|
||||
submitHandler={textSubmitHandler}
|
||||
maxLength={10}
|
||||
/>
|
||||
<div>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
@use '../../../theme/ontimeColours' as *;
|
||||
@use '../../../theme/ontimeStyles' as *;
|
||||
|
||||
.fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding-inline: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: $inner-section-text-size;
|
||||
color: $label-gray;
|
||||
}
|
||||
|
||||
.error {
|
||||
padding-inline: 0.5rem;
|
||||
font-size: $inner-section-text-size;
|
||||
color: $error-red;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { RenumberCues } from 'ontime-types';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { maybeAxiosError } from '../../../common/api/utils';
|
||||
import Button from '../../../common/components/buttons/Button';
|
||||
import Dialog from '../../../common/components/dialog/Dialog';
|
||||
import Input from '../../../common/components/input/input/Input';
|
||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||
import useRundown from '../../../common/hooks-query/useRundown';
|
||||
import { orderEntries } from '../rundown.utils';
|
||||
import { useEventSelection } from '../useEventSelection';
|
||||
|
||||
import style from './RenumberCuesDialog.module.scss';
|
||||
|
||||
type RenumberCueData = Pick<RenumberCues, 'increment' | 'prefix' | 'start'>;
|
||||
|
||||
export default function RenumberCuesDialog() {
|
||||
'use memo';
|
||||
const { data } = useRundown();
|
||||
const { flatOrder } = data;
|
||||
const { onClose, isOpen } = useRenumberCuesDialogStore();
|
||||
const { renumberCues } = useEntryActionsContext();
|
||||
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
clearErrors,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<RenumberCueData>();
|
||||
|
||||
const onSubmit = async (data: RenumberCueData) => {
|
||||
clearErrors();
|
||||
try {
|
||||
const { prefix, start, increment } = data;
|
||||
const orderedEvents = orderEntries(Array.from(selectedEvents), flatOrder);
|
||||
await renumberCues(orderedEvents, prefix, start, increment);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
const message = maybeAxiosError(error);
|
||||
setError('root', { message });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title='Renumber cues'
|
||||
showCloseButton
|
||||
showBackdrop
|
||||
bodyElements={
|
||||
<form id='renumber-cues-form' onSubmit={handleSubmit(onSubmit)} className={style.fields}>
|
||||
<div className={style.field}>
|
||||
<label className={style.label}>
|
||||
Prefix
|
||||
<Input
|
||||
{...register('prefix')}
|
||||
type='text'
|
||||
maxLength={8}
|
||||
height='large'
|
||||
fluid
|
||||
autoComplete='off'
|
||||
placeholder='A'
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className={style.field}>
|
||||
<label className={style.label}>
|
||||
Start
|
||||
<Input
|
||||
{...register('start')}
|
||||
type='number'
|
||||
required
|
||||
step={0.001}
|
||||
height='large'
|
||||
fluid
|
||||
autoComplete='off'
|
||||
placeholder='10'
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className={style.field}>
|
||||
<label className={style.label}>
|
||||
Increment
|
||||
<Input
|
||||
{...register('increment')}
|
||||
type='number'
|
||||
required
|
||||
step={0.001}
|
||||
height='large'
|
||||
fluid
|
||||
autoComplete='off'
|
||||
placeholder='0.1'
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{errors.root && <p className={style.error}>{errors.root.message}</p>}
|
||||
</form>
|
||||
}
|
||||
footerElements={
|
||||
<>
|
||||
<Button type='button' variant='subtle-white' onClick={onClose} disabled={isSubmitting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type='submit' variant='primary' form='renumber-cues-form' loading={isSubmitting}>
|
||||
Renumber
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface RenumberCuesDialogState {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onOpen: () => void;
|
||||
}
|
||||
|
||||
export const useRenumberCuesDialogStore = create<RenumberCuesDialogState>()((set) => ({
|
||||
isOpen: false,
|
||||
onClose: () => {
|
||||
set({ isOpen: false });
|
||||
},
|
||||
onOpen: () => {
|
||||
set({ isOpen: true });
|
||||
},
|
||||
}));
|
||||
@@ -13,13 +13,14 @@ import {
|
||||
IoTrash,
|
||||
IoUnlink,
|
||||
} from 'react-icons/io5';
|
||||
import { TbFlagFilled } from 'react-icons/tb';
|
||||
import { TbFlagFilled, TbListNumbers } from 'react-icons/tb';
|
||||
|
||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||
import { useContextMenu } from '../../../common/hooks/useContextMenu';
|
||||
import { useEntryCopy } from '../../../common/stores/entryCopyStore';
|
||||
import { deviceMod } from '../../../common/utils/deviceUtils';
|
||||
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||
import { useRenumberCuesDialogStore } from '../renumber-cues-dialog/RenumberCuesDialog';
|
||||
import { useEventIdSwapping } from '../useEventIdSwapping';
|
||||
import { getSelectionMode, useEventSelection } from '../useEventSelection';
|
||||
import RundownEventInner from './RundownEventInner';
|
||||
@@ -99,6 +100,7 @@ export default function RundownEvent({
|
||||
const selectedEventId = useEventIdSwapping((state) => state.selectedEventId);
|
||||
const setSelectedEventId = useEventIdSwapping((state) => state.setSelectedEventId);
|
||||
const clearSelectedEventId = useEventIdSwapping((state) => state.clearSelectedEventId);
|
||||
const openRenumberDialog = useRenumberCuesDialogStore((state) => state.onOpen);
|
||||
|
||||
const { updateEntry, batchUpdateEvents, clone, deleteEntry, groupEntries, swapEvents } = useEntryActionsContext();
|
||||
|
||||
@@ -143,6 +145,13 @@ export default function RundownEvent({
|
||||
disabled: parent !== null,
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
type: 'item',
|
||||
label: 'Renumber cues',
|
||||
icon: TbListNumbers,
|
||||
onClick: openRenumberDialog,
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
type: 'item',
|
||||
label: 'Delete',
|
||||
|
||||
Reference in New Issue
Block a user