mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-18 21:54:09 +00:00
feat: multiple selection (#703)
feat: multiple event selection --------- Co-authored-by: asharonbaltazar <asharonbaltazar@outlook.com> Co-authored-by: Alex <ac@omnivox.dk>
This commit is contained in:
@@ -38,6 +38,19 @@ export async function requestPutEvent(data: Partial<OntimeRundownEntry>) {
|
||||
return axios.put(rundownURL, data);
|
||||
}
|
||||
|
||||
type BatchEditEntry = {
|
||||
data: Partial<OntimeRundownEntry>;
|
||||
ids: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* @description HTTP request to put multiple events
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export async function requestBatchPutEvents(data: BatchEditEntry) {
|
||||
return axios.put(`${rundownURL}/batchEdit`, data);
|
||||
}
|
||||
|
||||
export type ReorderEntry = {
|
||||
eventId: string;
|
||||
from: number;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { MouseEvent } from 'react';
|
||||
import { IconButton, IconButtonProps, Tooltip } from '@chakra-ui/react';
|
||||
|
||||
interface TooltipActionBtnProps extends IconButtonProps {
|
||||
clickHandler: (event?: MouseEvent) => void | Promise<void>;
|
||||
clickHandler: (event: MouseEvent) => void | Promise<void>;
|
||||
tooltip: string;
|
||||
openDelay?: number;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// logic (with some modifications) culled from:
|
||||
// https://github.com/lukasbach/chakra-ui-contextmenu/blob/main/src/ContextMenu.tsx
|
||||
|
||||
import { Fragment, ReactElement } from 'react';
|
||||
import { Menu, MenuButton, MenuDivider, MenuItem, MenuList } from '@chakra-ui/react';
|
||||
import { ReactElement } from 'react';
|
||||
import { Menu, MenuButton, MenuGroup, MenuList } from '@chakra-ui/react';
|
||||
import { IconType } from '@react-icons/all-files';
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { ContextMenuOption } from './ContextMenuOption';
|
||||
|
||||
import style from './ContextMenu.module.scss';
|
||||
|
||||
type ContextMenuCoords = {
|
||||
@@ -13,14 +15,23 @@ type ContextMenuCoords = {
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type Option = {
|
||||
export type OptionWithoutGroup = {
|
||||
label: string;
|
||||
isDisabled?: boolean;
|
||||
icon: IconType;
|
||||
onClick: () => void;
|
||||
withDivider?: boolean;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
export type OptionWithGroup = {
|
||||
label: string;
|
||||
group: Omit<OptionWithoutGroup, 'isGroup'>[];
|
||||
};
|
||||
|
||||
export type Option = OptionWithoutGroup | OptionWithGroup;
|
||||
|
||||
const isOptionWithGroup = (option: Option): option is OptionWithGroup => 'group' in option;
|
||||
|
||||
type ContextMenuStore = {
|
||||
coords: ContextMenuCoords;
|
||||
options: Option[];
|
||||
@@ -69,14 +80,17 @@ export const ContextMenu = ({ children }: ContextMenuProps) => {
|
||||
}}
|
||||
/>
|
||||
<MenuList>
|
||||
{options.map(({ label, icon: Icon, onClick, withDivider, isDisabled }, i) => (
|
||||
<Fragment key={label}>
|
||||
{withDivider && <MenuDivider />}
|
||||
<MenuItem key={i} icon={<Icon />} onClick={onClick} isDisabled={isDisabled}>
|
||||
{label}
|
||||
</MenuItem>
|
||||
</Fragment>
|
||||
))}
|
||||
{options.map((option) =>
|
||||
isOptionWithGroup(option) ? (
|
||||
<MenuGroup key={option.label} title={option.label}>
|
||||
{option.group.map((groupOption) => (
|
||||
<ContextMenuOption key={groupOption.label} {...groupOption} />
|
||||
))}
|
||||
</MenuGroup>
|
||||
) : (
|
||||
<ContextMenuOption key={option.label} {...option} />
|
||||
),
|
||||
)}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { MenuDivider, MenuItem } from '@chakra-ui/react';
|
||||
|
||||
import { OptionWithoutGroup } from './ContextMenu';
|
||||
|
||||
export const ContextMenuOption = ({ label, onClick, isDisabled, icon: Icon, withDivider }: OptionWithoutGroup) => (
|
||||
<>
|
||||
{withDivider && <MenuDivider />}
|
||||
<MenuItem icon={<Icon />} onClick={onClick} isDisabled={isDisabled}>
|
||||
{label}
|
||||
</MenuItem>
|
||||
</>
|
||||
);
|
||||
@@ -8,6 +8,7 @@ import { logAxiosError } from '../api/apiUtils';
|
||||
import {
|
||||
ReorderEntry,
|
||||
requestApplyDelay,
|
||||
requestBatchPutEvents,
|
||||
requestDelete,
|
||||
requestDeleteAll,
|
||||
requestEventSwap,
|
||||
@@ -163,6 +164,59 @@ export const useEventAction = () => {
|
||||
[_updateEventMutation],
|
||||
);
|
||||
|
||||
/**
|
||||
* Calls mutation to edit multiple events
|
||||
* @private
|
||||
*/
|
||||
const _batchUpdateEventsMutation = useMutation({
|
||||
mutationFn: requestBatchPutEvents,
|
||||
onMutate: async ({ ids, data }) => {
|
||||
// cancel ongoing queries
|
||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousEvents = queryClient.getQueryData<GetRundownCached>(RUNDOWN);
|
||||
|
||||
if (previousEvents) {
|
||||
const updatedEvents = previousEvents.rundown.map((event) => {
|
||||
const isEventEdited = ids.includes(event.id);
|
||||
|
||||
if (isEventEdited && isOntimeEvent(event)) {
|
||||
return {
|
||||
...event,
|
||||
...data,
|
||||
};
|
||||
}
|
||||
|
||||
return event;
|
||||
});
|
||||
|
||||
queryClient.setQueryData(RUNDOWN, { rundown: updatedEvents, revision: -1 });
|
||||
}
|
||||
|
||||
// Return a context with the previous and new events
|
||||
return { previousEvents };
|
||||
},
|
||||
onSettled: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||
},
|
||||
onError: (_error, _newEvent, context) => {
|
||||
queryClient.setQueryData(RUNDOWN, context?.previousEvents);
|
||||
},
|
||||
networkMode: 'always',
|
||||
});
|
||||
|
||||
const batchUpdateEvents = useCallback(
|
||||
async (data: Partial<OntimeRundownEntry>, eventIds: string[]) => {
|
||||
try {
|
||||
await _batchUpdateEventsMutation.mutateAsync({ ids: eventIds, data });
|
||||
} catch (error) {
|
||||
logAxiosError('Error updating events', error);
|
||||
}
|
||||
},
|
||||
[_batchUpdateEventsMutation],
|
||||
);
|
||||
|
||||
/**
|
||||
* Calls mutation to delete an event
|
||||
* @private
|
||||
@@ -413,5 +467,6 @@ export const useEventAction = () => {
|
||||
applyDelay,
|
||||
reorderEvent,
|
||||
swapEvents,
|
||||
batchUpdateEvents,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -8,63 +8,27 @@ export enum AppMode {
|
||||
const appModeKey = 'ontime-app-mode';
|
||||
|
||||
function getModeFromSession() {
|
||||
return localStorage.getItem(appModeKey) === AppMode.Run ? AppMode.Run : AppMode.Edit;
|
||||
return sessionStorage.getItem(appModeKey) === AppMode.Run ? AppMode.Run : AppMode.Edit;
|
||||
}
|
||||
|
||||
async function persistModeToSession(mode: AppMode) {
|
||||
localStorage.setItem(appModeKey, mode);
|
||||
function persistModeToSession(mode: AppMode) {
|
||||
sessionStorage.setItem(appModeKey, mode);
|
||||
}
|
||||
|
||||
type AppModeStore = {
|
||||
mode: AppMode;
|
||||
cursor: string | null;
|
||||
editId: string | null;
|
||||
setMode: (mode: AppMode) => void;
|
||||
setCursor: (id: string | null, isEditable?: boolean) => void;
|
||||
setEditId: (id: string | null) => void;
|
||||
};
|
||||
|
||||
export const useAppMode = create<AppModeStore>()((set) => ({
|
||||
mode: getModeFromSession(),
|
||||
cursor: null,
|
||||
editId: null,
|
||||
setMode: (mode: AppMode) =>
|
||||
set((state) => {
|
||||
persistModeToSession(mode);
|
||||
return mode === AppMode.Edit
|
||||
? {
|
||||
editId: state.cursor,
|
||||
mode: mode,
|
||||
}
|
||||
: {
|
||||
editId: null,
|
||||
mode: mode,
|
||||
};
|
||||
}),
|
||||
setCursor: (id: string | null, isEditable?: boolean) =>
|
||||
set((state) => {
|
||||
if (isEditable) {
|
||||
return state.mode === AppMode.Edit
|
||||
? {
|
||||
cursor: id,
|
||||
editId: id,
|
||||
}
|
||||
: {
|
||||
cursor: id,
|
||||
};
|
||||
} else {
|
||||
return { cursor: id, editId: null };
|
||||
}
|
||||
}),
|
||||
setEditId: (id: string | null) =>
|
||||
set((state) => {
|
||||
return state.mode === AppMode.Edit
|
||||
? {
|
||||
cursor: id,
|
||||
editId: id,
|
||||
}
|
||||
: {
|
||||
editId: id,
|
||||
};
|
||||
}),
|
||||
setMode: (mode: AppMode) => {
|
||||
persistModeToSession(mode);
|
||||
|
||||
return set(() => {
|
||||
return { mode };
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user