Skip to content

Commit

Permalink
multi edit
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-Arc committed Aug 26, 2024
1 parent 1b1823e commit 3787422
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 112 deletions.
1 change: 1 addition & 0 deletions apps/client/src/common/utils/multiValueText.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const multipleValuesPlaceholder = '<Multiple Values>' as const;
4 changes: 2 additions & 2 deletions apps/client/src/features/rundown/RundownExport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useAppMode } from '../../common/stores/appModeStore';
import { handleLinks } from '../../common/utils/linkUtils';
import { cx } from '../../common/utils/styleUtils';

import EventEditor from './event-editor/EventEditor';
import EventEditorWrapper from './event-editor/EventEditorWrapper';
import RundownWrapper from './RundownWrapper';

import style from './RundownExport.module.scss';
Expand All @@ -33,7 +33,7 @@ const RundownExport = () => {
{!hideSideBar && (
<div className={style.side}>
<ErrorBoundary>
<EventEditor />
<EventEditorWrapper />
</ErrorBoundary>
</div>
)}
Expand Down
112 changes: 56 additions & 56 deletions apps/client/src/features/rundown/event-editor/EventEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,82 +1,77 @@
import { CSSProperties, useCallback, useEffect, useState } from 'react';
import { CSSProperties, useCallback } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Button } from '@chakra-ui/react';
import { CustomFieldLabel, isOntimeEvent, OntimeEvent } from 'ontime-types';
import { CustomFieldLabel, EventCustomFields, OntimeEvent } from 'ontime-types';

import CopyTag from '../../../common/components/copy-tag/CopyTag';
import { useEventAction } from '../../../common/hooks/useEventAction';
import useCustomFields from '../../../common/hooks-query/useCustomFields';
import useRundown from '../../../common/hooks-query/useRundown';
import { getAccessibleColour } from '../../../common/utils/styleUtils';
import { useEventSelection } from '../useEventSelection';

import EventEditorTimes from './composite/EventEditorTimes';
import EventEditorTitles from './composite/EventEditorTitles';
import EventTextArea from './composite/EventTextArea';
import EventEditorEmpty from './EventEditorEmpty';
import { getInitialAndPlaceholder } from './placeholderUtil';

import style from './EventEditor.module.scss';

export type EventEditorSubmitActions = keyof OntimeEvent;
export type MultiOntimeEvent = Omit<Partial<OntimeEvent>, 'id' | 'custom'> & {
id: string[];
custom: EventCustomFields;
};

export type EditorUpdateFields = 'cue' | 'title' | 'note' | 'colour' | CustomFieldLabel;

export default function EventEditor() {
const selectedEvents = useEventSelection((state) => state.selectedEvents);
const { data } = useRundown();
const { data: customFields } = useCustomFields();
const { order, rundown } = data;
const { updateEvent } = useEventAction();
const [_searchParams, setSearchParams] = useSearchParams();
interface EventEditorProps {
event: OntimeEvent;
isMultiple?: false;
}

const [event, setEvent] = useState<OntimeEvent | null>(null);
interface EventEditorMultiProps {
event: MultiOntimeEvent;
isMultiple: true;
}

useEffect(() => {
if (order.length === 0) {
setEvent(null);
return;
}
export default function EventEditor({ event, isMultiple }: EventEditorProps | EventEditorMultiProps) {
const { data: customFields } = useCustomFields();
const [_searchParams, setSearchParams] = useSearchParams();
const { updateEvent, batchUpdateEvents } = useEventAction();

const selectedEventId = order.find((eventId) => selectedEvents.has(eventId));
if (!selectedEventId) {
setEvent(null);
return;
}
const event = rundown[selectedEventId];
const handleOpenCustomManager = () => {
setSearchParams({ settings: 'feature_settings__custom' });
};
const idString = isMultiple ? event.id.join(', ') : event.id;

if (event && isOntimeEvent(event)) {
setEvent(event);
} else {
setEvent(null);
}
}, [order, rundown, selectedEvents]);
const submitCustomHandler = useCallback(
(field: CustomFieldLabel, value: string) => {
const fieldLabel = field.split('custom-')[1];
if (isMultiple) {
batchUpdateEvents({ custom: { [fieldLabel]: value } }, event.id);
} else {
updateEvent({ id: event.id, custom: { [fieldLabel]: value } });
}
},
[batchUpdateEvents, event, isMultiple, updateEvent],
);

const handleSubmit = useCallback(
const submitHandler = useCallback(
(field: EditorUpdateFields, value: string) => {
if (field.startsWith('custom-')) {
const fieldLabel = field.split('custom-')[1];
updateEvent({ id: event?.id, custom: { [fieldLabel]: value } });
if (isMultiple) {
batchUpdateEvents({ [field]: value }, event.id);
} else {
updateEvent({ id: event?.id, [field]: value });
updateEvent({ id: event.id, [field]: value });
}
},
[event?.id, updateEvent],
[batchUpdateEvents, event, isMultiple, updateEvent],
);

const handleOpenCustomManager = () => {
setSearchParams({ settings: 'feature_settings__custom' });
};

if (!event) {
return <EventEditorEmpty />;
}

return (
<div className={style.eventEditor} data-testid='editor-container'>
<div className={style.content}>
<EventEditorTimes
key={`${event.id}-times`}
eventId={event.id}
key={`${idString}-times`}
id={event.id}
timeStart={event.timeStart}
timeEnd={event.timeEnd}
duration={event.duration}
Expand All @@ -88,15 +83,17 @@ export default function EventEditor() {
timerType={event.timerType}
timeWarning={event.timeWarning}
timeDanger={event.timeDanger}
isMultiple
/>
<EventEditorTitles
key={`${event.id}-titles`}
eventId={event.id}
key={`${idString}-titles`}
eventId={idString}
cue={event.cue}
title={event.title}
note={event.note}
colour={event.colour}
handleSubmit={handleSubmit}
submitHandler={submitHandler}
isMultiple
/>
<div className={style.column}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
Expand All @@ -106,9 +103,9 @@ export default function EventEditor() {
</Button>
</div>
{Object.keys(customFields).map((fieldKey) => {
const key = `${event.id}-${fieldKey}`;
const key = `${idString}-${fieldKey}`;
const fieldName = `custom-${fieldKey}`;
const initialValue = event.custom[fieldKey] ?? '';
const [initialValue, placeholder] = getInitialAndPlaceholder(event.custom[fieldKey], isMultiple);
const { backgroundColor, color } = getAccessibleColour(customFields[fieldKey].colour);
const labelText = customFields[fieldKey].label;

Expand All @@ -118,18 +115,21 @@ export default function EventEditor() {
field={fieldName}
label={labelText}
initialValue={initialValue}
submitHandler={handleSubmit}
placeholder={placeholder}
submitHandler={submitCustomHandler}
className={style.decorated}
style={{ '--decorator-bg': backgroundColor, '--decorator-color': color } as CSSProperties}
/>
);
})}
</div>
</div>
<div className={style.footer}>
<CopyTag label='OSC trigger by id'>{`/ontime/load/id "${event.id}"`}</CopyTag>
<CopyTag label='OSC trigger by cue'>{`/ontime/load/cue "${event.cue}"`}</CopyTag>
</div>
{isMultiple ? null : (
<div className={style.footer}>
<CopyTag label='OSC trigger by id'>{`/ontime/load/id "${event.id}"`}</CopyTag>
<CopyTag label='OSC trigger by cue'>{`/ontime/load/cue "${event.cue}"`}</CopyTag>
</div>
)}
</div>
);
}
112 changes: 112 additions & 0 deletions apps/client/src/features/rundown/event-editor/EventEditorWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { isOntimeEvent, OntimeEvent, SupportedEvent } from 'ontime-types';

import useCustomFields from '../../../common/hooks-query/useCustomFields';
import useRundown from '../../../common/hooks-query/useRundown';
import { useEventSelection } from '../useEventSelection';

import EventEditor, { type MultiOntimeEvent } from './EventEditor';
import EventEditorEmpty from './EventEditorEmpty';

import style from './EventEditor.module.scss';

export default function EventEditorWrapper() {
const selectedEvents = useEventSelection((state) => state.selectedEvents);
const { data: customFields } = useCustomFields();
const { data } = useRundown();
const { order, rundown } = data;
const [_searchParams] = useSearchParams();

const [event, setEvent] = useState<OntimeEvent | null>(null);

useEffect(() => {
if (order.length === 0) {
setEvent(null);
return;
}

const selectedEventId = order.find((eventId) => selectedEvents.has(eventId));
if (!selectedEventId) {
setEvent(null);
return;
}
const event = rundown[selectedEventId];

if (event && isOntimeEvent(event)) {
setEvent(event);
} else {
setEvent(null);
}
}, [order, rundown, selectedEvents]);

if (!event) {
return <EventEditorEmpty />;
}

const getMultipleEvent = (): MultiOntimeEvent => {
const allHaveSameValue = (arr: OntimeEvent[], propertyName: keyof OntimeEvent) => {
if (!arr || arr.length <= 0) {
return false;
}
return arr.every((event) => event[propertyName] === arr[0][propertyName]);
};

const allHaveSameCustomValue = (arr: OntimeEvent[]) => {
if (!arr || arr.length <= 0) {
return false;
}

const collectedFields = Object.keys(customFields).reduce((acc, field) => {
if (arr.every((event) => event.custom[field] === arr[0].custom[field])) {
Object.assign(acc, { [field]: arr[0].custom[field] ?? '' });
} else {
Object.assign(acc, { [field]: undefined });
}
return acc;
}, {});
return collectedFields;
};

const events: OntimeEvent[] = [];
const eventsIds: string[] = [];

for (const entryId of selectedEvents) {
const data = rundown[entryId];
if (data && isOntimeEvent(data)) {
events.push(data);
eventsIds.push(entryId);
}
}

const multipleEvents: MultiOntimeEvent = {
ids: eventsIds,
colour: allHaveSameValue(events, 'colour') ? events[0]['colour'] : undefined,
custom: allHaveSameCustomValue(events),
duration: allHaveSameValue(events, 'duration') ? events[0]['duration'] : undefined,
endAction: allHaveSameValue(events, 'endAction') ? events[0]['endAction'] : undefined,
isPublic: allHaveSameValue(events, 'isPublic') ? events[0]['isPublic'] : undefined,
linkStart: allHaveSameValue(events, 'linkStart') ? events[0]['linkStart'] : undefined,
note: allHaveSameValue(events, 'note') ? events[0]['note'] : undefined,
revision: allHaveSameValue(events, 'revision') ? events[0]['revision'] : undefined,
skip: allHaveSameValue(events, 'skip') ? events[0]['skip'] : undefined,
timeDanger: allHaveSameValue(events, 'timeDanger') ? events[0]['timeDanger'] : undefined,
timeEnd: allHaveSameValue(events, 'timeEnd') ? events[0]['timeEnd'] : undefined,
timeStart: allHaveSameValue(events, 'timeStart') ? events[0]['timeStart'] : undefined,
timeStrategy: allHaveSameValue(events, 'timeStrategy') ? events[0]['timeStrategy'] : undefined,
timeWarning: allHaveSameValue(events, 'timeWarning') ? events[0]['timeWarning'] : undefined,
timerType: allHaveSameValue(events, 'timerType') ? events[0]['timerType'] : undefined,
title: allHaveSameValue(events, 'title') ? events[0]['title'] : undefined,
type: SupportedEvent.Event, //They are all ontime events
cue: allHaveSameValue(events, 'cue') ? events[0]['cue'] : undefined,
};

return multipleEvents;
};

return (
<div className={style.eventEditor} data-testid='editor-container'>
{selectedEvents.size <= 1 ? <EventEditor event={event} /> : <EventEditor event={getMultipleEvent()} isMultiple />}
</div>
);
}
Loading

0 comments on commit 3787422

Please sign in to comment.