mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-13 03:13:47 +00:00
chore: rename blocks to groups
This commit is contained in:
committed by
Carlos Valente
parent
486d89ecf4
commit
e5d2457717
@@ -95,14 +95,14 @@ export async function postCloneEntry(entryId: EntryId): Promise<AxiosResponse<Ru
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request for dissolving of a block
|
||||
* HTTP request for dissolving of a group
|
||||
*/
|
||||
export async function requestUngroup(blockId: EntryId): Promise<AxiosResponse<Rundown>> {
|
||||
return axios.post(`${rundownPath}/ungroup/${blockId}`);
|
||||
export async function requestUngroup(groupId: EntryId): Promise<AxiosResponse<Rundown>> {
|
||||
return axios.post(`${rundownPath}/ungroup/${groupId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request for grouping a list of entries into a block
|
||||
* HTTP request for grouping a list of entries into a group
|
||||
*/
|
||||
export async function requestGroupEntries(entryIds: EntryId[]): Promise<AxiosResponse<Rundown>> {
|
||||
return axios.post(`${rundownPath}/group`, { ids: entryIds });
|
||||
|
||||
@@ -2,13 +2,14 @@ import { useCallback } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
EntryId,
|
||||
isOntimeBlock,
|
||||
isOntimeEvent,
|
||||
isOntimeGroup,
|
||||
MaybeString,
|
||||
OntimeBlock,
|
||||
OntimeEntry,
|
||||
OntimeEvent,
|
||||
OntimeGroup,
|
||||
Rundown,
|
||||
SupportedEntry,
|
||||
TimeField,
|
||||
TimeStrategy,
|
||||
TransientEventPayload,
|
||||
@@ -36,7 +37,7 @@ import { logAxiosError } from '../api/utils';
|
||||
import { useEditorSettings } from '../stores/editorSettings';
|
||||
|
||||
export type EventOptions = Partial<{
|
||||
// options of any new entries (event / delay / block)
|
||||
// options of any new entries (event / delay / group)
|
||||
after: MaybeString;
|
||||
before: MaybeString;
|
||||
// options of entries of type OntimeEvent
|
||||
@@ -552,7 +553,7 @@ export const useEntryActions = () => {
|
||||
);
|
||||
|
||||
/**
|
||||
* Calls mutation to dissolve a block
|
||||
* Calls mutation to dissolve a group
|
||||
* @private
|
||||
*/
|
||||
const { mutateAsync: ungroupMutation } = useMutation({
|
||||
@@ -574,12 +575,12 @@ export const useEntryActions = () => {
|
||||
});
|
||||
|
||||
/**
|
||||
* Deletes a block and moves its events to the top level
|
||||
* Deletes a group and moves its events to the top level
|
||||
*/
|
||||
const ungroup = useCallback(
|
||||
async (blockId: EntryId) => {
|
||||
async (groupId: EntryId) => {
|
||||
try {
|
||||
await ungroupMutation(blockId);
|
||||
await ungroupMutation(groupId);
|
||||
} catch (error) {
|
||||
logAxiosError('Error dissolving group', error);
|
||||
}
|
||||
@@ -588,7 +589,7 @@ export const useEntryActions = () => {
|
||||
);
|
||||
|
||||
/**
|
||||
* Calls mutation to create a block with a selection
|
||||
* Calls mutation to create a group with a selection
|
||||
* @private
|
||||
*/
|
||||
const { mutateAsync: groupEntriesMutation } = useMutation({
|
||||
@@ -610,7 +611,7 @@ export const useEntryActions = () => {
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a block with a selection
|
||||
* Create a group with a selection
|
||||
*/
|
||||
const groupEntries = useCallback(
|
||||
async (entryIds: EntryId[]) => {
|
||||
@@ -674,8 +675,8 @@ export const useEntryActions = () => {
|
||||
} catch (error) {
|
||||
logAxiosError('Error re-ordering event', error);
|
||||
}
|
||||
// the rundown needs to know whether we moved into a block
|
||||
return rundown.entries[destinationId]?.type === 'block' ? destinationId : undefined;
|
||||
// the rundown needs to know whether we moved into a group
|
||||
return rundown.entries[destinationId]?.type === SupportedEntry.Group ? destinationId : undefined;
|
||||
},
|
||||
[queryClient, reorderEntryMutation],
|
||||
);
|
||||
@@ -798,12 +799,12 @@ function optimisticDeleteEntries(entryIds: EntryId[], rundown: Rundown) {
|
||||
}
|
||||
|
||||
function deleteEntry(entry: OntimeEntry) {
|
||||
if (isOntimeBlock(entry) || !entry.parent) {
|
||||
if (isOntimeGroup(entry) || !entry.parent) {
|
||||
order = order.filter((id) => id !== entry.id);
|
||||
} else {
|
||||
const parent = entries[entry.parent];
|
||||
if ('parent' in entries) {
|
||||
(parent as OntimeBlock).entries = (parent as OntimeBlock).entries.filter(
|
||||
(parent as OntimeGroup).entries = (parent as OntimeGroup).entries.filter(
|
||||
(parentEntry) => parentEntry !== entry.id,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ export const setClientRemote = {
|
||||
export const useRundownEditor = createSelector((state: RuntimeStore) => ({
|
||||
playback: state.timer.playback,
|
||||
selectedEventId: state.eventNow?.id ?? null,
|
||||
selectedBlockId: state.blockNow?.id ?? null,
|
||||
nextEventId: state.eventNext?.id ?? null,
|
||||
}));
|
||||
|
||||
@@ -131,8 +130,8 @@ export const useSelectedEventId = createSelector((state: RuntimeStore) => ({
|
||||
selectedEventId: state.eventNow?.id ?? null,
|
||||
}));
|
||||
|
||||
export const useCurrentBlockId = createSelector((state: RuntimeStore) => ({
|
||||
currentBlockId: state.blockNow?.id ?? null,
|
||||
export const useCurrentGroupId = createSelector((state: RuntimeStore) => ({
|
||||
currentGroupId: state.groupNow?.id ?? null,
|
||||
}));
|
||||
|
||||
export const setEventPlayback = {
|
||||
@@ -178,7 +177,7 @@ export const useRuntimePlaybackOverview = createSelector((state: RuntimeStore) =
|
||||
selectedEventIndex: state.runtime.selectedEventIndex,
|
||||
offset: state.runtime.offsetMode === OffsetMode.Absolute ? state.runtime.offsetAbs : state.runtime.offsetRel,
|
||||
|
||||
blockExpectedEnd: state.blockNow?.expectedEnd ?? null,
|
||||
groupExpectedEnd: state.groupNow?.expectedEnd ?? null,
|
||||
}));
|
||||
|
||||
export const useTimelineStatus = createSelector((state: RuntimeStore) => ({
|
||||
|
||||
+2
-2
@@ -25,8 +25,8 @@ const staticAutocompleteOptions = [
|
||||
'{{runtime.plannedEnd}}',
|
||||
'{{runtime.actualStart}}',
|
||||
'{{runtime.expectedEnd}}',
|
||||
'{{currentBlock.block}}',
|
||||
'{{currentBlock.startedAt}}',
|
||||
'{{currentGroup.id}}',
|
||||
'{{currentGroup.startedAt}}',
|
||||
];
|
||||
|
||||
const eventStaticPropertiesNow = [
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { Fragment } from 'react';
|
||||
import { IoLink } from 'react-icons/io5';
|
||||
import { CustomFields, isOntimeBlock, isOntimeEvent, Rundown } from 'ontime-types';
|
||||
import { CustomFields, isOntimeEvent, isOntimeGroup, Rundown } from 'ontime-types';
|
||||
import { millisToString } from 'ontime-utils';
|
||||
|
||||
import Tag from '../../../../../../common/components/tag/Tag';
|
||||
@@ -55,7 +55,7 @@ export default function PreviewRundown(props: PreviewRundownProps) {
|
||||
<tbody>
|
||||
{rundown.order.map((entryId) => {
|
||||
const entry = rundown.entries[entryId];
|
||||
if (isOntimeBlock(entry)) {
|
||||
if (isOntimeGroup(entry)) {
|
||||
return (
|
||||
<tr key={entry.id}>
|
||||
<td className={style.center}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { isOntimeBlock, isOntimeEvent, OntimeView } from 'ontime-types';
|
||||
import { isOntimeEvent, isOntimeGroup, OntimeView } from 'ontime-types';
|
||||
|
||||
import EmptyPage from '../../common/components/state/EmptyPage';
|
||||
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
||||
@@ -16,8 +16,8 @@ import { getDefaultFormat } from '../../common/utils/time';
|
||||
|
||||
import EditModal from './edit-modal/EditModal';
|
||||
import FollowButton from './follow-button/FollowButton';
|
||||
import OperatorBlock from './operator-block/OperatorBlock';
|
||||
import OperatorEvent from './operator-event/OperatorEvent';
|
||||
import OperatorGroup from './operator-group/OperatorGroup';
|
||||
import StatusBar from './status-bar/StatusBar';
|
||||
import { getOperatorOptions, useOperatorOptions } from './operator.options';
|
||||
import type { EditEvent } from './operator.types';
|
||||
@@ -168,10 +168,10 @@ export default function Operator() {
|
||||
);
|
||||
}
|
||||
|
||||
if (isOntimeBlock(entry)) {
|
||||
if (isOntimeGroup(entry)) {
|
||||
return (
|
||||
<Fragment key={entry.id}>
|
||||
<OperatorBlock key={entry.id} title={entry.title} />
|
||||
<OperatorGroup key={entry.id} title={entry.title} />
|
||||
{entry.entries.map((nestedEntryId) => {
|
||||
const nestedEntry = data.entries[nestedEntryId];
|
||||
if (!isOntimeEvent(nestedEntry)) {
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import style from './OperatorBlock.module.scss';
|
||||
|
||||
interface OperatorBlockProps {
|
||||
title: string;
|
||||
}
|
||||
|
||||
function OperatorBlock({ title }: OperatorBlockProps) {
|
||||
return <div className={style.block}>{title}</div>;
|
||||
}
|
||||
|
||||
export default memo(OperatorBlock);
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
.block {
|
||||
.group {
|
||||
width: 100%;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background-color: $gray-1350;
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
// tablet
|
||||
@media (min-width: $min-tablet) {
|
||||
.block {
|
||||
.group {
|
||||
padding: 0.25rem 1rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import style from './OperatorGroup.module.scss';
|
||||
|
||||
interface OperatorGroup {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export default memo(OperatorGroup);
|
||||
function OperatorGroup({ title }: OperatorGroup) {
|
||||
return <div className={style.group}>{title}</div>;
|
||||
}
|
||||
@@ -8,13 +8,13 @@ import {
|
||||
TbFolderPin,
|
||||
TbFolderStar,
|
||||
} from 'react-icons/tb';
|
||||
import { OntimeBlock, OntimeEvent, TimerPhase, TimerType } from 'ontime-types';
|
||||
import { OntimeEvent, OntimeGroup, TimerPhase, TimerType } from 'ontime-types';
|
||||
import { isPlaybackActive, millisToString } from 'ontime-utils';
|
||||
|
||||
import Tooltip from '../../../common/components/tooltip/Tooltip';
|
||||
import {
|
||||
useClock,
|
||||
useCurrentBlockId,
|
||||
useCurrentGroupId,
|
||||
useNextFlag,
|
||||
useRuntimeOverview,
|
||||
useRuntimePlaybackOverview,
|
||||
@@ -98,15 +98,15 @@ export function MetadataTimes() {
|
||||
|
||||
//TODO: there a some things here we still need to think about, mainly what to do whit the planed group duration in relation to the events
|
||||
function GroupTimes() {
|
||||
const { clock, blockExpectedEnd } = useRuntimePlaybackOverview();
|
||||
const { currentBlockId } = useCurrentBlockId();
|
||||
const group = useEntry(currentBlockId) as OntimeBlock | null;
|
||||
const { clock, groupExpectedEnd } = useRuntimePlaybackOverview();
|
||||
const { currentGroupId } = useCurrentGroupId();
|
||||
const group = useEntry(currentGroupId) as OntimeGroup | null;
|
||||
|
||||
// the group end time dose not encode any day offsets
|
||||
const plannedGroupEnd = group && group.timeStart !== null ? group.timeStart + group.duration - clock : null;
|
||||
const plannedTimeUntilGroupEnd = formattedTime(plannedGroupEnd, 3, TimerType.CountDown);
|
||||
|
||||
const expectedGroupEnd = blockExpectedEnd !== null ? blockExpectedEnd - clock : null;
|
||||
const expectedGroupEnd = groupExpectedEnd !== null ? groupExpectedEnd - clock : null;
|
||||
const expectedTimeUntilGroupEnd = formattedTime(expectedGroupEnd, 3, TimerType.CountDown);
|
||||
|
||||
const groupTitle = group?.title ?? null;
|
||||
@@ -120,7 +120,7 @@ function GroupTimes() {
|
||||
</div>
|
||||
<div className={style.labelledElement}>
|
||||
<Tooltip text='Time to expected group end' render={<TbFolderStar className={style.icon} />} />
|
||||
<span className={cx([style.time, blockExpectedEnd === null && style.muted])}>{expectedTimeUntilGroupEnd}</span>
|
||||
<span className={cx([style.time, groupExpectedEnd === null && style.muted])}>{expectedTimeUntilGroupEnd}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
type EntryId,
|
||||
type MaybeString,
|
||||
type Rundown,
|
||||
isOntimeBlock,
|
||||
isOntimeEvent,
|
||||
isOntimeGroup,
|
||||
OntimeEntry,
|
||||
Playback,
|
||||
SupportedEntry,
|
||||
@@ -25,9 +25,9 @@ import {
|
||||
import {
|
||||
getFirstNormal,
|
||||
getLastNormal,
|
||||
getNextBlockNormal,
|
||||
getNextGroupNormal,
|
||||
getNextNormal,
|
||||
getPreviousBlockNormal,
|
||||
getPreviousGroupNormal,
|
||||
getPreviousNormal,
|
||||
reorderArray,
|
||||
} from 'ontime-utils';
|
||||
@@ -41,8 +41,8 @@ import { AppMode, sessionKeys } from '../../ontimeConfig';
|
||||
|
||||
import QuickAddButtons from './entry-editor/quick-add-buttons/QuickAddButtons';
|
||||
import QuickAddInline from './entry-editor/quick-add-cursor/QuickAddInline';
|
||||
import RundownBlock from './rundown-block/RundownBlock';
|
||||
import RundownBlockEnd from './rundown-block/RundownBlockEnd';
|
||||
import RundownGroup from './rundown-group/RundownGroup';
|
||||
import RundownGroupEnd from './rundown-group/RundownGroupEnd';
|
||||
import { canDrop, makeRundownMetadata, makeSortableList } from './rundown.utils';
|
||||
import RundownEmpty from './RundownEmpty';
|
||||
import { useEventSelection } from './useEventSelection';
|
||||
@@ -127,7 +127,7 @@ export default function Rundown({ data }: RundownProps) {
|
||||
[addEntry],
|
||||
);
|
||||
|
||||
const selectBlock = useCallback(
|
||||
const selectGroup = useCallback(
|
||||
(cursor: string | null, direction: 'up' | 'down') => {
|
||||
if (order.length < 1) {
|
||||
return;
|
||||
@@ -137,7 +137,7 @@ export default function Rundown({ data }: RundownProps) {
|
||||
// there is no cursor, we select the first or last depending on direction
|
||||
const selected = direction === 'up' ? getLastNormal(entries, order) : getFirstNormal(entries, order);
|
||||
|
||||
if (isOntimeBlock(selected)) {
|
||||
if (isOntimeGroup(selected)) {
|
||||
setSelectedEvents({ id: selected.id, selectMode: 'click', index: direction === 'up' ? order.length : 0 });
|
||||
return;
|
||||
}
|
||||
@@ -151,8 +151,8 @@ export default function Rundown({ data }: RundownProps) {
|
||||
// otherwise we select the next or previous
|
||||
const selected =
|
||||
direction === 'up'
|
||||
? getPreviousBlockNormal(entries, order, newCursor)
|
||||
: getNextBlockNormal(entries, order, newCursor);
|
||||
? getPreviousGroupNormal(entries, order, newCursor)
|
||||
: getNextGroupNormal(entries, order, newCursor);
|
||||
|
||||
if (selected.entry !== null && selected.index !== null) {
|
||||
setSelectedEvents({ id: selected.entry.id, selectMode: 'click', index: selected.index });
|
||||
@@ -188,11 +188,11 @@ export default function Rundown({ data }: RundownProps) {
|
||||
);
|
||||
|
||||
/**
|
||||
* Checks whether a block is collapsed
|
||||
* Checks whether a group is collapsed
|
||||
*/
|
||||
const getIsCollapsed = useCallback(
|
||||
(blockId: EntryId): boolean => {
|
||||
return Boolean(collapsedGroups.find((id) => id === blockId));
|
||||
(groupId: EntryId): boolean => {
|
||||
return Boolean(collapsedGroups.find((id) => id === groupId));
|
||||
},
|
||||
[collapsedGroups],
|
||||
);
|
||||
@@ -223,10 +223,10 @@ export default function Rundown({ data }: RundownProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
const movedIntoBlockId = await move(cursor, direction);
|
||||
// if we are moving into a block, we need to make sure it is expanded
|
||||
if (movedIntoBlockId) {
|
||||
handleCollapseGroup(false, movedIntoBlockId);
|
||||
const movedIntoGroupId = await move(cursor, direction);
|
||||
// if we are moving into a group, we need to make sure it is expanded
|
||||
if (movedIntoGroupId) {
|
||||
handleCollapseGroup(false, movedIntoGroupId);
|
||||
}
|
||||
},
|
||||
[handleCollapseGroup, move],
|
||||
@@ -237,8 +237,8 @@ export default function Rundown({ data }: RundownProps) {
|
||||
['alt + ArrowDown', () => selectEntry(cursor, 'down'), { preventDefault: true, usePhysicalKeys: true }],
|
||||
['alt + ArrowUp', () => selectEntry(cursor, 'up'), { preventDefault: true, usePhysicalKeys: true }],
|
||||
|
||||
['alt + shift + ArrowDown', () => selectBlock(cursor, 'down'), { preventDefault: true, usePhysicalKeys: true }],
|
||||
['alt + shift + ArrowUp', () => selectBlock(cursor, 'up'), { preventDefault: true, usePhysicalKeys: true }],
|
||||
['alt + shift + ArrowDown', () => selectGroup(cursor, 'down'), { preventDefault: true, usePhysicalKeys: true }],
|
||||
['alt + shift + ArrowUp', () => selectGroup(cursor, 'up'), { preventDefault: true, usePhysicalKeys: true }],
|
||||
|
||||
['alt + mod + ArrowDown', () => moveEntry(cursor, 'down'), { preventDefault: true, usePhysicalKeys: true }],
|
||||
['alt + mod + ArrowUp', () => moveEntry(cursor, 'up'), { preventDefault: true, usePhysicalKeys: true }],
|
||||
@@ -260,12 +260,12 @@ export default function Rundown({ data }: RundownProps) {
|
||||
|
||||
[
|
||||
'alt + G',
|
||||
() => insertAtId({ type: SupportedEntry.Block }, cursor),
|
||||
() => insertAtId({ type: SupportedEntry.Group }, cursor),
|
||||
{ preventDefault: true, usePhysicalKeys: true },
|
||||
],
|
||||
[
|
||||
'alt + shift + G',
|
||||
() => insertAtId({ type: SupportedEntry.Block }, cursor, true),
|
||||
() => insertAtId({ type: SupportedEntry.Group }, cursor, true),
|
||||
{ preventDefault: true, usePhysicalKeys: true },
|
||||
],
|
||||
|
||||
@@ -335,7 +335,10 @@ export default function Rundown({ data }: RundownProps) {
|
||||
}
|
||||
|
||||
// prevent dropping a group inside another
|
||||
if (active.data.current?.type === 'block' && !canDrop(over.data.current?.type, over.data.current?.parent)) {
|
||||
if (
|
||||
active.data.current?.type === SupportedEntry.Group &&
|
||||
!canDrop(over.data.current?.type, over.data.current?.parent)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -346,10 +349,10 @@ export default function Rundown({ data }: RundownProps) {
|
||||
let order: 'before' | 'after' | 'insert' = fromIndex < toIndex ? 'after' : 'before';
|
||||
|
||||
/**
|
||||
* We need to specially handle the end blocks
|
||||
* Dragging before and end block will add the entry to the end of the block
|
||||
* Dragging after an end block will add the event after the block itself
|
||||
* Dragging to the top of a block either place before first entry or if no entries do insert
|
||||
* We need to specially handle the end-group
|
||||
* Dragging before a end-group will add the entry to the end of the group
|
||||
* Dragging after a end-group will add the event after the group itself
|
||||
* Dragging to the top of a group either place before first entry or if no entries do insert
|
||||
*/
|
||||
if (destinationId.startsWith('end-')) {
|
||||
destinationId = destinationId.replace('end-', '');
|
||||
@@ -358,11 +361,11 @@ export default function Rundown({ data }: RundownProps) {
|
||||
order = 'insert';
|
||||
}
|
||||
} else {
|
||||
const block = data.entries[destinationId];
|
||||
if (isOntimeBlock(block) && order === 'after') {
|
||||
if (block.entries.length === 0) order = 'insert';
|
||||
const group = data.entries[destinationId];
|
||||
if (isOntimeGroup(group) && order === 'after') {
|
||||
if (group.entries.length === 0) order = 'insert';
|
||||
else {
|
||||
destinationId = block.entries[0];
|
||||
destinationId = group.entries[0];
|
||||
order = 'before';
|
||||
}
|
||||
}
|
||||
@@ -380,31 +383,31 @@ export default function Rundown({ data }: RundownProps) {
|
||||
};
|
||||
|
||||
/**
|
||||
* When we drag a block, we force collapse it
|
||||
* This avoids strange scenarios like dropping a block inside itself
|
||||
* When we drag a group, we force collapse it
|
||||
* This avoids strange scenarios like dropping a group inside itself
|
||||
*/
|
||||
const collapseDraggedBlocks = (event: DragStartEvent) => {
|
||||
const isBlock = event.active.data.current?.type === 'block';
|
||||
if (isBlock) {
|
||||
const collapseDraggedGroups = (event: DragStartEvent) => {
|
||||
const isGroup = event.active.data.current?.type === SupportedEntry.Group;
|
||||
if (isGroup) {
|
||||
handleCollapseGroup(true, event.active.id as EntryId);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* When we drag over a block, we expand it if it is collapsed
|
||||
* When we drag over a group, we expand it if it is collapsed
|
||||
*/
|
||||
const expandOverBlock = (event: DragOverEvent) => {
|
||||
// if we are dragging a block, the drop operation is invalid so we dont expand
|
||||
if (event.active.data.current?.type === 'block') {
|
||||
const expandOverGroup = (event: DragOverEvent) => {
|
||||
// if we are dragging a group, the drop operation is invalid so we dont expand
|
||||
if (event.active.data.current?.type === 'group') {
|
||||
return;
|
||||
}
|
||||
if (event.over?.data.current?.type !== 'block') {
|
||||
if (event.over?.data.current?.type !== 'group') {
|
||||
return;
|
||||
}
|
||||
const blockId = event.over?.id as EntryId;
|
||||
const isCollapsed = getIsCollapsed(blockId);
|
||||
const groupId = event.over?.id as EntryId;
|
||||
const isCollapsed = getIsCollapsed(groupId);
|
||||
if (isCollapsed) {
|
||||
handleCollapseGroup(false, blockId);
|
||||
handleCollapseGroup(false, groupId);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -424,37 +427,37 @@ export default function Rundown({ data }: RundownProps) {
|
||||
<div className={style.rundownContainer} ref={scrollRef} data-testid='rundown'>
|
||||
<DndContext
|
||||
onDragEnd={handleOnDragEnd}
|
||||
onDragStart={collapseDraggedBlocks}
|
||||
onDragOver={expandOverBlock}
|
||||
onDragStart={collapseDraggedGroups}
|
||||
onDragOver={expandOverGroup}
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
>
|
||||
<SortableContext items={sortableData} strategy={verticalListSortingStrategy}>
|
||||
<div className={style.list}>
|
||||
{isEditMode && <QuickAddButtons previousEventId={null} parentBlock={null} />}
|
||||
{isEditMode && <QuickAddButtons previousEventId={null} parentGroup={null} />}
|
||||
{sortableData.map((entryId, index) => {
|
||||
// the entry might be a pseudo block-end which does not generate metadata and should not be processed
|
||||
// the entry might be a pseudo end-group which does not generate metadata and should not be processed
|
||||
if (entryId.startsWith('end-')) {
|
||||
const parentId = entryId.split('end-')[1];
|
||||
const isBlockCollapsed = getIsCollapsed(parentId);
|
||||
const isGroupCollapsed = getIsCollapsed(parentId);
|
||||
|
||||
if (isBlockCollapsed) {
|
||||
if (isGroupCollapsed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// if the previous element is selected, it will have its own QuickAddInline
|
||||
// we use thisId instead of previousEntryId because the block end does not process
|
||||
// we use thisId instead of previousEntryId because the end-group does not process
|
||||
// and it does not cause the reassignment of the iteration id to the previous entry
|
||||
return (
|
||||
<Fragment key={entryId}>
|
||||
{isEditMode && rundownMetadata.groupEntries === 0 && (
|
||||
<QuickAddButtons
|
||||
previousEventId={null}
|
||||
parentBlock={parentId}
|
||||
parentGroup={parentId}
|
||||
backgroundColor={rundownMetadata.groupColour}
|
||||
/>
|
||||
)}
|
||||
<RundownBlockEnd key={entryId} id={entryId} colour={rundownMetadata.groupColour} />
|
||||
<RundownGroupEnd key={entryId} id={entryId} colour={rundownMetadata.groupColour} />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
@@ -469,7 +472,7 @@ export default function Rundown({ data }: RundownProps) {
|
||||
|
||||
// if the entry has a parent, and it is collapsed, render nothing
|
||||
if (
|
||||
entry.type !== SupportedEntry.Block &&
|
||||
entry.type !== SupportedEntry.Group &&
|
||||
rundownMetadata.groupId !== null &&
|
||||
getIsCollapsed(rundownMetadata.groupId)
|
||||
) {
|
||||
@@ -480,12 +483,12 @@ export default function Rundown({ data }: RundownProps) {
|
||||
const hasCursor = entry.id === cursor;
|
||||
|
||||
/**
|
||||
* Outside a block, the value will be undefined
|
||||
* Outside a group, the value will be undefined
|
||||
* If the colour is empty string ''
|
||||
* ie: we are inside a block, but there is no defined colour
|
||||
* ie: we are inside a group, but there is no defined colour
|
||||
* we default to $gray-500 #9d9d9d
|
||||
*/
|
||||
const blockColour = rundownMetadata.groupColour === '' ? '#9d9d9d' : rundownMetadata.groupColour;
|
||||
const groupColour = rundownMetadata.groupColour === '' ? '#9d9d9d' : rundownMetadata.groupColour;
|
||||
|
||||
const isFirst = index === 0;
|
||||
const isLast = entryId === order.at(-1);
|
||||
@@ -510,10 +513,10 @@ export default function Rundown({ data }: RundownProps) {
|
||||
* - if it is not the first entry (the buttons would be there)
|
||||
*/}
|
||||
{isEditMode && hasCursor && !isFirst && (
|
||||
<QuickAddInline previousEventId={rundownMetadata.previousEntryId} parentBlock={parentIdForBefore} />
|
||||
<QuickAddInline previousEventId={rundownMetadata.previousEntryId} parentGroup={parentIdForBefore} />
|
||||
)}
|
||||
{isOntimeBlock(entry) ? (
|
||||
<RundownBlock
|
||||
{isOntimeGroup(entry) ? (
|
||||
<RundownGroup
|
||||
data={entry}
|
||||
hasCursor={hasCursor}
|
||||
collapsed={getIsCollapsed(entry.id)}
|
||||
@@ -523,7 +526,7 @@ export default function Rundown({ data }: RundownProps) {
|
||||
<div
|
||||
className={style.entryWrapper}
|
||||
data-testid={`entry-${rundownMetadata.eventIndex}`}
|
||||
style={blockColour ? { '--user-bg': blockColour } : {}}
|
||||
style={groupColour ? { '--user-bg': groupColour } : {}}
|
||||
>
|
||||
{isOntimeEvent(entry) && (
|
||||
<div className={style.entryIndex}>
|
||||
@@ -556,16 +559,16 @@ export default function Rundown({ data }: RundownProps) {
|
||||
* - edit mode only
|
||||
* - if there is a cursor
|
||||
* - if it is not the last entry (the buttons would be there)
|
||||
* - if the entry is not the block header
|
||||
* - if the entry is not the group header
|
||||
*/}
|
||||
{isEditMode && hasCursor && !isLast && (
|
||||
<QuickAddInline previousEventId={entry.id} parentBlock={parentIdForAfter} />
|
||||
<QuickAddInline previousEventId={entry.id} parentGroup={parentIdForAfter} />
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
{isEditMode && (
|
||||
<QuickAddButtons previousEventId={rundownMetadata.groupId ?? rundownMetadata.thisId} parentBlock={null} />
|
||||
<QuickAddButtons previousEventId={rundownMetadata.groupId ?? rundownMetadata.thisId} parentGroup={null} />
|
||||
)}
|
||||
<div className={style.spacer} />
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function RundownEmpty(props: RundownEmptyProps) {
|
||||
|
||||
<Editor.Separator />
|
||||
|
||||
<Button onClick={() => handleAddNew(SupportedEntry.Block)} variant='primary' size='large'>
|
||||
<Button onClick={() => handleAddNew(SupportedEntry.Group)} variant='primary' size='large'>
|
||||
<IoAdd /> Create Group
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -25,12 +25,12 @@ export type EventItemActions =
|
||||
| 'event-before'
|
||||
| 'delay'
|
||||
| 'delay-before'
|
||||
| 'block'
|
||||
| 'block-before'
|
||||
| 'group'
|
||||
| 'group-before'
|
||||
| 'swap'
|
||||
| 'delete'
|
||||
| 'clone'
|
||||
| 'group'
|
||||
| 'make-group'
|
||||
| 'update';
|
||||
|
||||
interface RundownEntryProps {
|
||||
@@ -106,11 +106,11 @@ export default function RundownEntry({
|
||||
case 'delay-before': {
|
||||
return addEntry({ type: SupportedEntry.Delay }, { after: previousEntryId });
|
||||
}
|
||||
case 'block': {
|
||||
return addEntry({ type: SupportedEntry.Block }, { after: data.id });
|
||||
case 'group': {
|
||||
return addEntry({ type: SupportedEntry.Group }, { after: data.id });
|
||||
}
|
||||
case 'block-before': {
|
||||
return addEntry({ type: SupportedEntry.Block }, { after: previousEntryId });
|
||||
case 'group-before': {
|
||||
return addEntry({ type: SupportedEntry.Group }, { after: previousEntryId });
|
||||
}
|
||||
case 'swap': {
|
||||
const { value } = payload as FieldValue;
|
||||
@@ -129,7 +129,7 @@ export default function RundownEntry({
|
||||
addEntry(newEvent, { after: data.id });
|
||||
break;
|
||||
}
|
||||
case 'group': {
|
||||
case 'make-group': {
|
||||
if (selectedEvents.size > 1) {
|
||||
clearMultiSelection();
|
||||
return groupEntries(Array.from(selectedEvents));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EntryId, OntimeBlock, OntimeDelay, OntimeEvent, RundownEntries, SupportedEntry } from 'ontime-types';
|
||||
import { EntryId, OntimeDelay, OntimeEvent, OntimeGroup, RundownEntries, SupportedEntry } from 'ontime-types';
|
||||
|
||||
import { makeRundownMetadata, makeSortableList, moveDown, moveUp, orderEntries } from '../rundown.utils';
|
||||
|
||||
@@ -18,16 +18,16 @@ describe('makeRundownMetadata()', () => {
|
||||
skip: false,
|
||||
linkStart: false,
|
||||
} as OntimeEvent,
|
||||
block: {
|
||||
id: 'block',
|
||||
type: SupportedEntry.Block,
|
||||
group: {
|
||||
id: 'group',
|
||||
type: SupportedEntry.Group,
|
||||
entries: ['11', 'delay', '12', '13'],
|
||||
colour: 'red',
|
||||
} as OntimeBlock,
|
||||
} as OntimeGroup,
|
||||
'11': {
|
||||
id: '11',
|
||||
type: SupportedEntry.Event,
|
||||
parent: 'block',
|
||||
parent: 'group',
|
||||
timeStart: 10,
|
||||
timeEnd: 11,
|
||||
duration: 1,
|
||||
@@ -39,13 +39,13 @@ describe('makeRundownMetadata()', () => {
|
||||
delay: {
|
||||
id: 'delay',
|
||||
type: SupportedEntry.Delay,
|
||||
parent: 'block',
|
||||
parent: 'group',
|
||||
duration: 0,
|
||||
} as OntimeDelay,
|
||||
'12': {
|
||||
id: '12',
|
||||
type: SupportedEntry.Event,
|
||||
parent: 'block',
|
||||
parent: 'group',
|
||||
timeStart: 11,
|
||||
timeEnd: 12,
|
||||
duration: 1,
|
||||
@@ -57,7 +57,7 @@ describe('makeRundownMetadata()', () => {
|
||||
'13': {
|
||||
id: '13',
|
||||
type: SupportedEntry.Event,
|
||||
parent: 'block',
|
||||
parent: 'group',
|
||||
timeStart: 12,
|
||||
timeEnd: 13,
|
||||
duration: 1,
|
||||
@@ -114,25 +114,25 @@ describe('makeRundownMetadata()', () => {
|
||||
groupEntries: undefined,
|
||||
});
|
||||
|
||||
expect(process(demoEvents['block'])).toMatchObject({
|
||||
expect(process(demoEvents['group'])).toMatchObject({
|
||||
previousEvent: demoEvents['1'],
|
||||
latestEvent: demoEvents['1'],
|
||||
previousEntryId: demoEvents['1'].id,
|
||||
thisId: demoEvents['block'].id,
|
||||
thisId: demoEvents['group'].id,
|
||||
eventIndex: 1,
|
||||
isPast: true,
|
||||
isNextDay: false,
|
||||
totalGap: 0,
|
||||
isLinkedToLoaded: false,
|
||||
isLoaded: false,
|
||||
groupId: 'block',
|
||||
groupId: 'group',
|
||||
groupColour: 'red',
|
||||
});
|
||||
|
||||
expect(process(demoEvents['11'])).toMatchObject({
|
||||
previousEvent: demoEvents['1'],
|
||||
latestEvent: demoEvents['11'],
|
||||
previousEntryId: demoEvents['block'].id,
|
||||
previousEntryId: demoEvents['group'].id,
|
||||
thisId: demoEvents['11'].id,
|
||||
eventIndex: 2,
|
||||
isPast: true,
|
||||
@@ -140,7 +140,7 @@ describe('makeRundownMetadata()', () => {
|
||||
totalGap: 10,
|
||||
isLinkedToLoaded: false,
|
||||
isLoaded: false,
|
||||
groupId: 'block',
|
||||
groupId: 'group',
|
||||
groupColour: 'red',
|
||||
});
|
||||
|
||||
@@ -155,7 +155,7 @@ describe('makeRundownMetadata()', () => {
|
||||
totalGap: 10,
|
||||
isLinkedToLoaded: false,
|
||||
isLoaded: false,
|
||||
groupId: 'block',
|
||||
groupId: 'group',
|
||||
groupColour: 'red',
|
||||
});
|
||||
|
||||
@@ -170,7 +170,7 @@ describe('makeRundownMetadata()', () => {
|
||||
totalGap: 10,
|
||||
isLinkedToLoaded: false,
|
||||
isLoaded: true,
|
||||
groupId: 'block',
|
||||
groupId: 'group',
|
||||
groupColour: 'red',
|
||||
});
|
||||
|
||||
@@ -185,7 +185,7 @@ describe('makeRundownMetadata()', () => {
|
||||
totalGap: 10,
|
||||
isLinkedToLoaded: true,
|
||||
isLoaded: false,
|
||||
groupId: 'block',
|
||||
groupId: 'group',
|
||||
groupColour: 'red',
|
||||
});
|
||||
|
||||
@@ -205,18 +205,18 @@ describe('makeRundownMetadata()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('populates previousEntries in blocks', () => {
|
||||
const rundownStartsWithBlock = {
|
||||
block: {
|
||||
id: 'block',
|
||||
type: SupportedEntry.Block,
|
||||
it('populates previousEntries in groups', () => {
|
||||
const rundownStartsWithGroup = {
|
||||
group: {
|
||||
id: 'group',
|
||||
type: SupportedEntry.Group,
|
||||
colour: 'red',
|
||||
entries: ['1', '2'],
|
||||
} as OntimeBlock,
|
||||
} as OntimeGroup,
|
||||
'1': {
|
||||
id: '1',
|
||||
type: SupportedEntry.Event,
|
||||
parent: 'block',
|
||||
parent: 'group',
|
||||
timeStart: 1,
|
||||
timeEnd: 2,
|
||||
duration: 1,
|
||||
@@ -228,7 +228,7 @@ describe('makeRundownMetadata()', () => {
|
||||
'2': {
|
||||
id: '2',
|
||||
type: SupportedEntry.Event,
|
||||
parent: 'block',
|
||||
parent: 'group',
|
||||
timeStart: 2,
|
||||
timeEnd: 3,
|
||||
duration: 1,
|
||||
@@ -240,49 +240,49 @@ describe('makeRundownMetadata()', () => {
|
||||
};
|
||||
const { process } = makeRundownMetadata(null);
|
||||
|
||||
expect(process(rundownStartsWithBlock.block)).toStrictEqual({
|
||||
expect(process(rundownStartsWithGroup.group)).toStrictEqual({
|
||||
previousEvent: null,
|
||||
latestEvent: null,
|
||||
previousEntryId: null,
|
||||
thisId: rundownStartsWithBlock.block.id,
|
||||
thisId: rundownStartsWithGroup.group.id,
|
||||
eventIndex: 0,
|
||||
isPast: false,
|
||||
isNextDay: false,
|
||||
totalGap: 0,
|
||||
isLinkedToLoaded: false,
|
||||
isLoaded: false,
|
||||
groupId: rundownStartsWithBlock.block.id,
|
||||
groupId: rundownStartsWithGroup.group.id,
|
||||
groupColour: 'red',
|
||||
groupEntries: 2,
|
||||
});
|
||||
|
||||
expect(process(rundownStartsWithBlock['1'])).toStrictEqual({
|
||||
expect(process(rundownStartsWithGroup['1'])).toStrictEqual({
|
||||
previousEvent: null,
|
||||
latestEvent: rundownStartsWithBlock['1'],
|
||||
previousEntryId: rundownStartsWithBlock.block.id,
|
||||
thisId: rundownStartsWithBlock['1'].id,
|
||||
latestEvent: rundownStartsWithGroup['1'],
|
||||
previousEntryId: rundownStartsWithGroup.group.id,
|
||||
thisId: rundownStartsWithGroup['1'].id,
|
||||
eventIndex: 1,
|
||||
isPast: false,
|
||||
isNextDay: false,
|
||||
totalGap: 0,
|
||||
isLinkedToLoaded: false,
|
||||
isLoaded: false,
|
||||
groupId: rundownStartsWithBlock.block.id,
|
||||
groupId: rundownStartsWithGroup.group.id,
|
||||
groupColour: 'red',
|
||||
groupEntries: 2,
|
||||
});
|
||||
expect(process(rundownStartsWithBlock['2'])).toStrictEqual({
|
||||
previousEvent: rundownStartsWithBlock['1'],
|
||||
latestEvent: rundownStartsWithBlock['2'],
|
||||
previousEntryId: rundownStartsWithBlock['1'].id,
|
||||
thisId: rundownStartsWithBlock['2'].id,
|
||||
expect(process(rundownStartsWithGroup['2'])).toStrictEqual({
|
||||
previousEvent: rundownStartsWithGroup['1'],
|
||||
latestEvent: rundownStartsWithGroup['2'],
|
||||
previousEntryId: rundownStartsWithGroup['1'].id,
|
||||
thisId: rundownStartsWithGroup['2'].id,
|
||||
eventIndex: 2,
|
||||
isPast: false,
|
||||
isNextDay: false,
|
||||
totalGap: 0,
|
||||
isLinkedToLoaded: false,
|
||||
isLoaded: false,
|
||||
groupId: rundownStartsWithBlock.block.id,
|
||||
groupId: rundownStartsWithGroup.group.id,
|
||||
groupColour: 'red',
|
||||
groupEntries: 2,
|
||||
});
|
||||
@@ -290,52 +290,52 @@ describe('makeRundownMetadata()', () => {
|
||||
});
|
||||
|
||||
describe('makeSortableList()', () => {
|
||||
it('generates a list with block ends', () => {
|
||||
const order = ['block-1', '2', 'block-3', 'block-4'];
|
||||
it('generates a list with group ends', () => {
|
||||
const order = ['group-1', '2', 'group-3', 'group-4'];
|
||||
const entries: RundownEntries = {
|
||||
'block-1': { type: SupportedEntry.Block, id: 'block-1', entries: ['11'] } as OntimeBlock,
|
||||
'11': { type: SupportedEntry.Event, id: '11', parent: 'block-1' } as OntimeEvent,
|
||||
'group-1': { type: SupportedEntry.Group, id: 'group-1', entries: ['11'] } as OntimeGroup,
|
||||
'11': { type: SupportedEntry.Event, id: '11', parent: 'group-1' } as OntimeEvent,
|
||||
'2': { type: SupportedEntry.Event, id: '2', parent: null } as OntimeEvent,
|
||||
'block-3': { type: SupportedEntry.Block, id: 'block-3', entries: ['31'] } as OntimeBlock,
|
||||
'31': { type: SupportedEntry.Event, id: '31', parent: 'block-3' } as OntimeEvent,
|
||||
'block-4': { type: SupportedEntry.Block, id: 'block-4', entries: [] as string[] } as OntimeBlock,
|
||||
'group-3': { type: SupportedEntry.Group, id: 'group-3', entries: ['31'] } as OntimeGroup,
|
||||
'31': { type: SupportedEntry.Event, id: '31', parent: 'group-3' } as OntimeEvent,
|
||||
'group-4': { type: SupportedEntry.Group, id: 'group-4', entries: [] as string[] } as OntimeGroup,
|
||||
};
|
||||
|
||||
const sortableList = makeSortableList(order, entries);
|
||||
expect(sortableList).toStrictEqual([
|
||||
'block-1',
|
||||
'group-1',
|
||||
'11',
|
||||
'end-block-1',
|
||||
'end-group-1',
|
||||
'2',
|
||||
'block-3',
|
||||
'group-3',
|
||||
'31',
|
||||
'end-block-3',
|
||||
'block-4',
|
||||
'end-block-4',
|
||||
'end-group-3',
|
||||
'group-4',
|
||||
'end-group-4',
|
||||
]);
|
||||
});
|
||||
|
||||
it('closes dangling blocks', () => {
|
||||
const order = ['block'];
|
||||
it('closes dangling group', () => {
|
||||
const order = ['group'];
|
||||
const entries: RundownEntries = {
|
||||
block: { type: SupportedEntry.Block, id: 'block-1', entries: ['11', '12'] } as OntimeBlock,
|
||||
'11': { type: SupportedEntry.Event, id: '11', parent: 'block-1' } as OntimeEvent,
|
||||
'12': { type: SupportedEntry.Event, id: '12', parent: 'block-1' } as OntimeEvent,
|
||||
group: { type: SupportedEntry.Group, id: 'group-1', entries: ['11', '12'] } as OntimeGroup,
|
||||
'11': { type: SupportedEntry.Event, id: '11', parent: 'group-1' } as OntimeEvent,
|
||||
'12': { type: SupportedEntry.Event, id: '12', parent: 'group-1' } as OntimeEvent,
|
||||
};
|
||||
|
||||
const sortableList = makeSortableList(order, entries);
|
||||
expect(sortableList).toStrictEqual(['block-1', '11', '12', 'end-block-1']);
|
||||
expect(sortableList).toStrictEqual(['group-1', '11', '12', 'end-group-1']);
|
||||
});
|
||||
|
||||
it('handles a list with a with just blocks', () => {
|
||||
const order = ['block-1', 'block-2'];
|
||||
it('handles a list with a with just groups', () => {
|
||||
const order = ['group-1', 'group-2'];
|
||||
const entries: RundownEntries = {
|
||||
'block-1': { type: SupportedEntry.Block, id: 'block-1', entries: [] as string[] } as OntimeBlock,
|
||||
'block-2': { type: SupportedEntry.Block, id: 'block-2', entries: [] as string[] } as OntimeBlock,
|
||||
'group-1': { type: SupportedEntry.Group, id: 'group-1', entries: [] as string[] } as OntimeGroup,
|
||||
'group-2': { type: SupportedEntry.Group, id: 'group-2', entries: [] as string[] } as OntimeGroup,
|
||||
};
|
||||
|
||||
const sortableList = makeSortableList(order, entries);
|
||||
expect(sortableList).toStrictEqual(['block-1', 'end-block-1', 'block-2', 'end-block-2']);
|
||||
expect(sortableList).toStrictEqual(['group-1', 'end-group-1', 'group-2', 'end-group-2']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -345,15 +345,15 @@ describe('moveUp()', () => {
|
||||
'1': { id: '1', type: 'event', parent: null } as OntimeEvent,
|
||||
'2': { id: '2', type: 'event', parent: null } as OntimeEvent,
|
||||
'3': { id: '3', type: 'event', parent: null } as OntimeEvent,
|
||||
block: { id: 'block', type: 'block', entries: ['11', '12'] } as OntimeBlock,
|
||||
'11': { id: '11', type: 'event', parent: 'block' } as OntimeEvent,
|
||||
'12': { id: '12', type: 'event', parent: 'block' } as OntimeEvent,
|
||||
group: { id: 'group', type: 'group', entries: ['11', '12'] } as OntimeGroup,
|
||||
'11': { id: '11', type: 'event', parent: 'group' } as OntimeEvent,
|
||||
'12': { id: '12', type: 'event', parent: 'group' } as OntimeEvent,
|
||||
'4': { id: '4', type: 'event', parent: null } as OntimeEvent,
|
||||
block2: { id: 'block2', type: 'block', entries: [] as EntryId[] } as OntimeBlock,
|
||||
group2: { id: 'group2', type: 'group', entries: [] as EntryId[] } as OntimeGroup,
|
||||
'5': { id: '5', type: 'event', parent: null } as OntimeEvent,
|
||||
},
|
||||
order: ['1', '2', '3', 'block', '4', 'block2', '5'],
|
||||
flatOrder: ['1', '2', '3', 'block', '11', '12', '4', 'block2', '5'],
|
||||
order: ['1', '2', '3', 'group', '4', 'group2', '5'],
|
||||
flatOrder: ['1', '2', '3', 'group', '11', '12', '4', 'group2', '5'],
|
||||
};
|
||||
|
||||
it('moving the first event is a noop', () => {
|
||||
@@ -370,7 +370,7 @@ describe('moveUp()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('moves an entry up inside a block', () => {
|
||||
it('moves an entry up inside a group', () => {
|
||||
expect(moveUp('12', rundown.flatOrder, rundown.entries)).toStrictEqual({
|
||||
destinationId: '11',
|
||||
order: 'before',
|
||||
@@ -379,7 +379,7 @@ describe('moveUp()', () => {
|
||||
|
||||
it('moves an entry up into an empty group', () => {
|
||||
expect(moveUp('5', rundown.flatOrder, rundown.entries)).toStrictEqual({
|
||||
destinationId: 'block2',
|
||||
destinationId: 'group2',
|
||||
order: 'insert',
|
||||
});
|
||||
});
|
||||
@@ -393,45 +393,45 @@ describe('moveUp()', () => {
|
||||
|
||||
it('moves an entry up out of a group', () => {
|
||||
expect(moveUp('11', rundown.flatOrder, rundown.entries)).toStrictEqual({
|
||||
destinationId: 'block',
|
||||
destinationId: 'group',
|
||||
order: 'before',
|
||||
});
|
||||
});
|
||||
|
||||
it('moves a block in the rundown', () => {
|
||||
expect(moveUp('block', rundown.flatOrder, rundown.entries)).toStrictEqual({
|
||||
it('moves a group in the rundown', () => {
|
||||
expect(moveUp('group', rundown.flatOrder, rundown.entries)).toStrictEqual({
|
||||
destinationId: '3',
|
||||
order: 'before',
|
||||
});
|
||||
});
|
||||
|
||||
it('swaps two blocks', () => {
|
||||
it('swaps two groups', () => {
|
||||
const rundown = {
|
||||
entries: {
|
||||
block: { id: 'block', type: 'block', entries: ['11'] } as OntimeBlock,
|
||||
'11': { id: '11', type: 'event', parent: 'block' } as OntimeEvent,
|
||||
block2: { id: 'block2', type: 'block', entries: [] as EntryId[] } as OntimeBlock,
|
||||
group: { id: 'group', type: 'group', entries: ['11'] } as OntimeGroup,
|
||||
'11': { id: '11', type: 'event', parent: 'group' } as OntimeEvent,
|
||||
group2: { id: 'group2', type: 'group', entries: [] as EntryId[] } as OntimeGroup,
|
||||
},
|
||||
order: ['block', 'block2'],
|
||||
flatOrder: ['block', '11', 'block2'],
|
||||
order: ['group', 'group2'],
|
||||
flatOrder: ['group', '11', 'group2'],
|
||||
};
|
||||
expect(moveUp('block2', rundown.flatOrder, rundown.entries)).toStrictEqual({
|
||||
destinationId: 'block',
|
||||
expect(moveUp('group2', rundown.flatOrder, rundown.entries)).toStrictEqual({
|
||||
destinationId: 'group',
|
||||
order: 'before',
|
||||
});
|
||||
});
|
||||
|
||||
it('moves before a block', () => {
|
||||
it('moves before a group', () => {
|
||||
const rundown = {
|
||||
entries: {
|
||||
block: { id: 'block', type: 'block', entries: ['11'] } as OntimeBlock,
|
||||
'11': { id: '11', type: 'event', parent: 'block' } as OntimeEvent,
|
||||
group: { id: 'group', type: 'group', entries: ['11'] } as OntimeGroup,
|
||||
'11': { id: '11', type: 'event', parent: 'group' } as OntimeEvent,
|
||||
},
|
||||
order: ['block'],
|
||||
flatOrder: ['block', '11'],
|
||||
order: ['group'],
|
||||
flatOrder: ['group', '11'],
|
||||
};
|
||||
expect(moveUp('11', rundown.flatOrder, rundown.entries)).toStrictEqual({
|
||||
destinationId: 'block',
|
||||
destinationId: 'group',
|
||||
order: 'before',
|
||||
});
|
||||
});
|
||||
@@ -443,15 +443,15 @@ describe('moveDown()', () => {
|
||||
'1': { id: '1', type: 'event', parent: null } as OntimeEvent,
|
||||
'2': { id: '2', type: 'event', parent: null } as OntimeEvent,
|
||||
'3': { id: '3', type: 'event', parent: null } as OntimeEvent,
|
||||
block: { id: 'block', type: 'block', entries: ['11', '12'] } as OntimeBlock,
|
||||
'11': { id: '11', type: 'event', parent: 'block' } as OntimeEvent,
|
||||
'12': { id: '12', type: 'event', parent: 'block' } as OntimeEvent,
|
||||
group: { id: 'group', type: 'group', entries: ['11', '12'] } as OntimeGroup,
|
||||
'11': { id: '11', type: 'event', parent: 'group' } as OntimeEvent,
|
||||
'12': { id: '12', type: 'event', parent: 'group' } as OntimeEvent,
|
||||
'4': { id: '4', type: 'event', parent: null } as OntimeEvent,
|
||||
block2: { id: 'block2', type: 'block', entries: [] as EntryId[] } as OntimeBlock,
|
||||
group2: { id: 'group2', type: 'group', entries: [] as EntryId[] } as OntimeGroup,
|
||||
'5': { id: '5', type: 'event', parent: null } as OntimeEvent,
|
||||
},
|
||||
order: ['1', '2', '3', 'block', '4', 'block2', '5'],
|
||||
flatOrder: ['1', '2', '3', 'block', '11', '12', '4', 'block2', '5'],
|
||||
order: ['1', '2', '3', 'group', '4', 'group2', '5'],
|
||||
flatOrder: ['1', '2', '3', 'group', '11', '12', '4', 'group2', '5'],
|
||||
};
|
||||
|
||||
it('moving the last event is a noop', () => {
|
||||
@@ -468,7 +468,7 @@ describe('moveDown()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('moves an entry down inside a block', () => {
|
||||
it('moves an entry down inside a group', () => {
|
||||
expect(moveDown('11', rundown.flatOrder, rundown.entries)).toStrictEqual({
|
||||
destinationId: '12',
|
||||
order: 'after',
|
||||
@@ -477,14 +477,14 @@ describe('moveDown()', () => {
|
||||
|
||||
it('moves an entry down into an empty group', () => {
|
||||
expect(moveDown('4', rundown.flatOrder, rundown.entries)).toStrictEqual({
|
||||
destinationId: 'block2',
|
||||
destinationId: 'group2',
|
||||
order: 'insert',
|
||||
});
|
||||
});
|
||||
|
||||
it('moves an entry down out of a group', () => {
|
||||
expect(moveDown('12', rundown.flatOrder, rundown.entries)).toStrictEqual({
|
||||
destinationId: 'block',
|
||||
destinationId: 'group',
|
||||
order: 'after',
|
||||
});
|
||||
});
|
||||
@@ -496,40 +496,40 @@ describe('moveDown()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('moves a block in the rundown', () => {
|
||||
expect(moveDown('block', rundown.flatOrder, rundown.entries)).toStrictEqual({
|
||||
it('moves a group in the rundown', () => {
|
||||
expect(moveDown('group', rundown.flatOrder, rundown.entries)).toStrictEqual({
|
||||
destinationId: '4',
|
||||
order: 'after',
|
||||
});
|
||||
});
|
||||
|
||||
it('swaps two blocks', () => {
|
||||
it('swaps two groups', () => {
|
||||
const rundown = {
|
||||
entries: {
|
||||
block: { id: 'block', type: 'block', entries: ['11'] } as OntimeBlock,
|
||||
'11': { id: '11', type: 'event', parent: 'block' } as OntimeEvent,
|
||||
block2: { id: 'block2', type: 'block', entries: [] as EntryId[] } as OntimeBlock,
|
||||
group: { id: 'group', type: 'group', entries: ['11'] } as OntimeGroup,
|
||||
'11': { id: '11', type: 'event', parent: 'group' } as OntimeEvent,
|
||||
group2: { id: 'group2', type: 'group', entries: [] as EntryId[] } as OntimeGroup,
|
||||
},
|
||||
order: ['block', 'block2'],
|
||||
flatOrder: ['block', '11', 'block2'],
|
||||
order: ['group', 'group2'],
|
||||
flatOrder: ['group', '11', 'group2'],
|
||||
};
|
||||
expect(moveDown('block', rundown.flatOrder, rundown.entries)).toStrictEqual({
|
||||
destinationId: 'block2',
|
||||
expect(moveDown('group', rundown.flatOrder, rundown.entries)).toStrictEqual({
|
||||
destinationId: 'group2',
|
||||
order: 'after',
|
||||
});
|
||||
});
|
||||
|
||||
it('moves after a block', () => {
|
||||
it('moves after a group', () => {
|
||||
const rundown = {
|
||||
entries: {
|
||||
block: { id: 'block', type: 'block', entries: ['11'] } as OntimeBlock,
|
||||
'11': { id: '11', type: 'event', parent: 'block' } as OntimeEvent,
|
||||
group: { id: 'group', type: 'group', entries: ['11'] } as OntimeGroup,
|
||||
'11': { id: '11', type: 'event', parent: 'group' } as OntimeEvent,
|
||||
},
|
||||
order: ['block'],
|
||||
flatOrder: ['block', '11'],
|
||||
order: ['group'],
|
||||
flatOrder: ['group', '11'],
|
||||
};
|
||||
expect(moveDown('11', rundown.flatOrder, rundown.entries)).toStrictEqual({
|
||||
destinationId: 'block',
|
||||
destinationId: 'group',
|
||||
order: 'after',
|
||||
});
|
||||
});
|
||||
|
||||
+5
-6
@@ -9,13 +9,12 @@ import style from './TitleEditor.module.scss';
|
||||
|
||||
interface TitleEditorProps {
|
||||
title: string;
|
||||
eventId: string;
|
||||
entryId: string;
|
||||
placeholder: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function EditableBlockTitle(props: TitleEditorProps) {
|
||||
const { title, eventId, placeholder, className } = props;
|
||||
export default function TitleEditor({ title, entryId, placeholder, className }: TitleEditorProps) {
|
||||
const { updateEntry } = useEntryActions();
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
const submitCallback = useCallback(
|
||||
@@ -25,9 +24,9 @@ export default function EditableBlockTitle(props: TitleEditorProps) {
|
||||
}
|
||||
|
||||
const cleanVal = text.trim();
|
||||
updateEntry({ id: eventId, title: cleanVal });
|
||||
updateEntry({ id: entryId, title: cleanVal });
|
||||
},
|
||||
[title, updateEntry, eventId],
|
||||
[title, updateEntry, entryId],
|
||||
);
|
||||
|
||||
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(title, submitCallback, ref, {
|
||||
@@ -38,7 +37,7 @@ export default function EditableBlockTitle(props: TitleEditorProps) {
|
||||
|
||||
return (
|
||||
<Input
|
||||
data-testid='block__title'
|
||||
data-testid='entry__title'
|
||||
variant='ghosted'
|
||||
fluid
|
||||
ref={ref}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isOntimeBlock, isOntimeEvent, OntimeEntry } from 'ontime-types';
|
||||
import { isOntimeEvent, isOntimeGroup, OntimeEntry } from 'ontime-types';
|
||||
|
||||
import useRundown from '../../../common/hooks-query/useRundown';
|
||||
|
||||
import BlockEditor from './BlockEditor';
|
||||
import EventEditor from './EventEditor';
|
||||
import GroupEditor from './GroupEditor';
|
||||
|
||||
import style from './EntryEditor.module.scss';
|
||||
|
||||
@@ -38,10 +38,10 @@ export default function CuesheetEntryEditor({ entryId }: CuesheetEntryEditorProp
|
||||
);
|
||||
}
|
||||
|
||||
if (isOntimeBlock(entry)) {
|
||||
if (isOntimeGroup(entry)) {
|
||||
return (
|
||||
<div className={style.inModal} data-testid='editor-container'>
|
||||
<BlockEditor block={entry} />
|
||||
<GroupEditor group={entry} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+22
-22
@@ -1,5 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
import { MaybeNumber, OntimeBlock } from 'ontime-types';
|
||||
import { MaybeNumber, OntimeGroup } from 'ontime-types';
|
||||
import { millisToString } from 'ontime-utils';
|
||||
|
||||
import * as Editor from '../../../common/components/editor-utils/EditorUtils';
|
||||
@@ -19,38 +19,38 @@ import TargetDurationInput from './composite/TargetDurationInput';
|
||||
import style from './EntryEditor.module.scss';
|
||||
|
||||
// title + colour + custom field labels
|
||||
export type BlockEditorUpdateTextFields = 'title' | 'colour' | string;
|
||||
export type BlockEditorUpdateMaybeNumberFields = 'targetDuration';
|
||||
export type GroupEditorUpdateTextFields = 'title' | 'colour' | string;
|
||||
export type GroupEditorUpdateMaybeNumberFields = 'targetDuration';
|
||||
|
||||
interface BlockEditorProps {
|
||||
block: OntimeBlock;
|
||||
interface GroupEditorProps {
|
||||
group: OntimeGroup;
|
||||
}
|
||||
|
||||
export default function BlockEditor({ block }: BlockEditorProps) {
|
||||
export default function GroupEditor({ group }: GroupEditorProps) {
|
||||
const { data: customFields } = useCustomFields();
|
||||
const { updateEntry } = useEntryActions();
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(field: BlockEditorUpdateTextFields | BlockEditorUpdateMaybeNumberFields, value: string | MaybeNumber) => {
|
||||
(field: GroupEditorUpdateTextFields | GroupEditorUpdateMaybeNumberFields, value: string | MaybeNumber) => {
|
||||
// Handle custom fields
|
||||
if (typeof field === 'string' && field.startsWith('custom-')) {
|
||||
const fieldLabel = field.split('custom-')[1];
|
||||
updateEntry({ id: block.id, custom: { [fieldLabel]: value as string } });
|
||||
updateEntry({ id: group.id, custom: { [fieldLabel]: value as string } });
|
||||
return;
|
||||
}
|
||||
|
||||
if (field === 'targetDuration') {
|
||||
return updateEntry({ id: block.id, targetDuration: value as MaybeNumber });
|
||||
return updateEntry({ id: group.id, targetDuration: value as MaybeNumber });
|
||||
}
|
||||
|
||||
// all other strings are text fields
|
||||
return updateEntry({ id: block.id, [field]: value as string });
|
||||
return updateEntry({ id: group.id, [field]: value as string });
|
||||
},
|
||||
[block.id, updateEntry],
|
||||
[group.id, updateEntry],
|
||||
);
|
||||
|
||||
const isEditor = window.location.pathname.includes('editor');
|
||||
const planOffset = typeof block.targetDuration !== 'number' ? null : block.duration - block.targetDuration;
|
||||
const planOffset = typeof group.targetDuration !== 'number' ? null : group.duration - group.targetDuration;
|
||||
const planOffsetLabel = planOffset !== null ? getOffsetState(planOffset * -1) : null;
|
||||
|
||||
return (
|
||||
@@ -64,19 +64,19 @@ export default function BlockEditor({ block }: BlockEditorProps) {
|
||||
}
|
||||
<Editor.Label>First event start</Editor.Label>
|
||||
<TextLikeInput className={style.textLikeInput}>
|
||||
{millisToString(block.timeStart, { fallback: timerPlaceholder })}
|
||||
{millisToString(group.timeStart, { fallback: timerPlaceholder })}
|
||||
</TextLikeInput>
|
||||
</div>
|
||||
<div>
|
||||
<Editor.Label>Last event end</Editor.Label>
|
||||
<TextLikeInput className={style.textLikeInput}>
|
||||
{millisToString(block.timeEnd, { fallback: timerPlaceholder })}
|
||||
{millisToString(group.timeEnd, { fallback: timerPlaceholder })}
|
||||
</TextLikeInput>
|
||||
</div>
|
||||
<div>
|
||||
<Editor.Label htmlFor='duration'>Scheduled duration</Editor.Label>
|
||||
<TextLikeInput className={style.textLikeInput}>
|
||||
{millisToString(block.duration, { fallback: enDash })}
|
||||
{millisToString(group.duration, { fallback: enDash })}
|
||||
</TextLikeInput>
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,21 +93,21 @@ export default function BlockEditor({ block }: BlockEditorProps) {
|
||||
</TextLikeInput>
|
||||
</div>
|
||||
<TargetDurationInput
|
||||
duration={block.duration}
|
||||
targetDuration={block.targetDuration}
|
||||
duration={group.duration}
|
||||
targetDuration={group.targetDuration}
|
||||
submitHandler={handleSubmit}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={style.column}>
|
||||
<Editor.Title>Block data</Editor.Title>
|
||||
<Editor.Title>Group data</Editor.Title>
|
||||
<div>
|
||||
<Editor.Label>Colour</Editor.Label>
|
||||
<SwatchSelect name='colour' value={block.colour} handleChange={handleSubmit} />
|
||||
<SwatchSelect name='colour' value={group.colour} handleChange={handleSubmit} />
|
||||
</div>
|
||||
<EntryEditorTextInput field='title' label='Title' initialValue={block.title} submitHandler={handleSubmit} />
|
||||
<EventTextArea field='note' label='Note' initialValue={block.note} submitHandler={handleSubmit} />
|
||||
<EntryEditorTextInput field='title' label='Title' initialValue={group.title} submitHandler={handleSubmit} />
|
||||
<EventTextArea field='note' label='Note' initialValue={group.note} submitHandler={handleSubmit} />
|
||||
</div>
|
||||
|
||||
<div className={style.column}>
|
||||
@@ -115,7 +115,7 @@ export default function BlockEditor({ block }: BlockEditorProps) {
|
||||
Custom Fields
|
||||
{isEditor && <AppLink search='settings=manage__custom'>Manage Custom Fields</AppLink>}
|
||||
</Editor.Title>
|
||||
<EntryEditorCustomFields fields={customFields} handleSubmit={handleSubmit} entry={block} />
|
||||
<EntryEditorCustomFields fields={customFields} handleSubmit={handleSubmit} entry={group} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
isOntimeBlock,
|
||||
isOntimeDelay,
|
||||
isOntimeEvent,
|
||||
isOntimeGroup,
|
||||
isOntimeMilestone,
|
||||
OntimeBlock,
|
||||
OntimeEvent,
|
||||
OntimeGroup,
|
||||
OntimeMilestone,
|
||||
} from 'ontime-types';
|
||||
|
||||
@@ -13,9 +13,9 @@ import useRundown from '../../../common/hooks-query/useRundown';
|
||||
import { useEventSelection } from '../useEventSelection';
|
||||
|
||||
import EventEditorFooter from './composite/EventEditorFooter';
|
||||
import BlockEditor from './BlockEditor';
|
||||
import EventEditor from './EventEditor';
|
||||
import EventEditorEmpty from './EventEditorEmpty';
|
||||
import GroupEditor from './GroupEditor';
|
||||
import MilestoneEditor from './MilestoneEditor';
|
||||
|
||||
import style from './EntryEditor.module.scss';
|
||||
@@ -24,7 +24,7 @@ export default function RundownEntryEditor() {
|
||||
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
||||
const { data } = useRundown();
|
||||
|
||||
const [entry, setEntry] = useState<OntimeEvent | OntimeBlock | OntimeMilestone | null>(null);
|
||||
const [entry, setEntry] = useState<OntimeEvent | OntimeGroup | OntimeMilestone | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (data.order.length === 0) {
|
||||
@@ -67,10 +67,10 @@ export default function RundownEntryEditor() {
|
||||
);
|
||||
}
|
||||
|
||||
if (isOntimeBlock(entry)) {
|
||||
if (isOntimeGroup(entry)) {
|
||||
return (
|
||||
<div className={style.entryEditor} data-testid='editor-container'>
|
||||
<BlockEditor block={entry} />
|
||||
<GroupEditor group={entry} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CSSProperties, Fragment } from 'react';
|
||||
import { CustomFields, OntimeBlock, OntimeEvent, OntimeMilestone } from 'ontime-types';
|
||||
import { CustomFields, OntimeEvent, OntimeGroup, OntimeMilestone } from 'ontime-types';
|
||||
|
||||
import { getAccessibleColour } from '../../../../common/utils/styleUtils';
|
||||
import { EventEditorUpdateFields } from '../EventEditor';
|
||||
@@ -12,7 +12,7 @@ import style from '../EntryEditor.module.scss';
|
||||
|
||||
interface EntryEditorCustomFieldsProps {
|
||||
fields: CustomFields;
|
||||
entry: OntimeEvent | OntimeBlock | OntimeMilestone;
|
||||
entry: OntimeEvent | OntimeGroup | OntimeMilestone;
|
||||
handleSubmit: (field: EventEditorUpdateFields, value: string) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@ import { useCallback, useRef } from 'react';
|
||||
import * as Editor from '../../../../common/components/editor-utils/EditorUtils';
|
||||
import Input, { type InputProps } from '../../../../common/components/input/input/Input';
|
||||
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
|
||||
import { BlockEditorUpdateTextFields } from '../BlockEditor';
|
||||
import { EventEditorUpdateFields } from '../EventEditor';
|
||||
import { GroupEditorUpdateTextFields } from '../GroupEditor';
|
||||
|
||||
interface EntryEditorTextInputProps extends InputProps {
|
||||
field: EventEditorUpdateFields | BlockEditorUpdateTextFields;
|
||||
field: EventEditorUpdateFields | GroupEditorUpdateTextFields;
|
||||
label: string;
|
||||
initialValue: string;
|
||||
placeholder?: string;
|
||||
|
||||
@@ -17,27 +17,27 @@ interface TargetDurationInputProps {
|
||||
}
|
||||
|
||||
export default function TargetDurationInput({ duration, targetDuration, submitHandler }: TargetDurationInputProps) {
|
||||
const isBlocked = targetDuration !== null;
|
||||
const isLocked = targetDuration !== null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Editor.Label htmlFor='targetDuration'>Target duration</Editor.Label>
|
||||
<TimeInputGroup hasDelay={isBlocked && targetDuration !== duration}>
|
||||
<TimeInputGroup hasDelay={isLocked && targetDuration !== duration}>
|
||||
<NullableTimeInput
|
||||
name='targetDuration'
|
||||
time={targetDuration}
|
||||
submitHandler={submitHandler}
|
||||
emptyDisplay={enDash}
|
||||
className={isBlocked ? '' : style.inactive}
|
||||
className={isLocked ? '' : style.inactive}
|
||||
/>
|
||||
<Tooltip
|
||||
text='Lock to target duration'
|
||||
className={cx([style.timeAction, isBlocked && style.active])}
|
||||
onClick={() => submitHandler('targetDuration', isBlocked ? null : duration)}
|
||||
className={cx([style.timeAction, isLocked && style.active])}
|
||||
onClick={() => submitHandler('targetDuration', isLocked ? null : duration)}
|
||||
data-testid='lock__duration'
|
||||
render={<IconButton variant='subtle-white' className={isBlocked ? style.active : style.inactive} />}
|
||||
render={<IconButton variant='subtle-white' className={isLocked ? style.active : style.inactive} />}
|
||||
>
|
||||
{isBlocked ? <IoLockClosed /> : <IoLockOpenOutline />}
|
||||
{isLocked ? <IoLockClosed /> : <IoLockOpenOutline />}
|
||||
</Tooltip>
|
||||
</TimeInputGroup>
|
||||
</div>
|
||||
|
||||
+14
-14
@@ -11,19 +11,19 @@ import style from './QuickAddButtons.module.scss';
|
||||
|
||||
interface QuickAddButtonsProps {
|
||||
previousEventId: MaybeString;
|
||||
parentBlock: MaybeString;
|
||||
parentGroup: MaybeString;
|
||||
backgroundColor?: string;
|
||||
}
|
||||
|
||||
export default memo(QuickAddButtons);
|
||||
function QuickAddButtons({ previousEventId, parentBlock, backgroundColor }: QuickAddButtonsProps) {
|
||||
function QuickAddButtons({ previousEventId, parentGroup, backgroundColor }: QuickAddButtonsProps) {
|
||||
const { addEntry } = useEntryActions();
|
||||
|
||||
const addEvent = () => {
|
||||
addEntry(
|
||||
{
|
||||
type: SupportedEntry.Event,
|
||||
parent: parentBlock,
|
||||
parent: parentGroup,
|
||||
},
|
||||
{
|
||||
after: previousEventId,
|
||||
@@ -34,7 +34,7 @@ function QuickAddButtons({ previousEventId, parentBlock, backgroundColor }: Quic
|
||||
|
||||
const addDelay = () => {
|
||||
addEntry(
|
||||
{ type: SupportedEntry.Delay, parent: parentBlock },
|
||||
{ type: SupportedEntry.Delay, parent: parentGroup },
|
||||
{
|
||||
lastEventId: previousEventId,
|
||||
after: previousEventId,
|
||||
@@ -44,7 +44,7 @@ function QuickAddButtons({ previousEventId, parentBlock, backgroundColor }: Quic
|
||||
|
||||
const addMilestone = () => {
|
||||
addEntry(
|
||||
{ type: SupportedEntry.Milestone, parent: parentBlock },
|
||||
{ type: SupportedEntry.Milestone, parent: parentGroup },
|
||||
{
|
||||
lastEventId: previousEventId,
|
||||
after: previousEventId,
|
||||
@@ -52,12 +52,12 @@ function QuickAddButtons({ previousEventId, parentBlock, backgroundColor }: Quic
|
||||
);
|
||||
};
|
||||
|
||||
const addBlock = () => {
|
||||
if (parentBlock !== null) {
|
||||
const addGroup = () => {
|
||||
if (parentGroup !== null) {
|
||||
return;
|
||||
}
|
||||
addEntry(
|
||||
{ type: SupportedEntry.Block },
|
||||
{ type: SupportedEntry.Group },
|
||||
{
|
||||
lastEventId: previousEventId,
|
||||
after: previousEventId,
|
||||
@@ -67,15 +67,15 @@ function QuickAddButtons({ previousEventId, parentBlock, backgroundColor }: Quic
|
||||
|
||||
/**
|
||||
* If the colour is empty string ''
|
||||
* ie: we are inside a block, but there is no defined colour
|
||||
* ie: we are inside a group, but there is no defined colour
|
||||
* we default to $gray-500 #9d9d9d
|
||||
*/
|
||||
const blockColour = backgroundColor === '' ? '#9d9d9d' : backgroundColor;
|
||||
const groupColour = backgroundColor === '' ? '#9d9d9d' : backgroundColor;
|
||||
|
||||
return (
|
||||
<Toolbar.Root
|
||||
className={cx([style.quickAdd, Boolean(parentBlock) && style.indent])}
|
||||
style={blockColour ? { '--user-bg': blockColour } : {}}
|
||||
className={cx([style.quickAdd, Boolean(parentGroup) && style.indent])}
|
||||
style={groupColour ? { '--user-bg': groupColour } : {}}
|
||||
data-testid='quick-add-buttons'
|
||||
>
|
||||
<Toolbar.Button render={<Button size='small' />} onClick={addEvent}>
|
||||
@@ -93,8 +93,8 @@ function QuickAddButtons({ previousEventId, parentBlock, backgroundColor }: Quic
|
||||
Milestone
|
||||
</Toolbar.Button>
|
||||
|
||||
{parentBlock === null && (
|
||||
<Toolbar.Button render={<Button size='small' />} onClick={addBlock}>
|
||||
{parentGroup === null && (
|
||||
<Toolbar.Button render={<Button size='small' />} onClick={addGroup}>
|
||||
<IoAdd />
|
||||
Group
|
||||
</Toolbar.Button>
|
||||
|
||||
@@ -10,18 +10,18 @@ import style from './QuickAddInline.module.scss';
|
||||
|
||||
interface QuickAddInlineProps {
|
||||
previousEventId: MaybeString;
|
||||
parentBlock: MaybeString;
|
||||
parentGroup: MaybeString;
|
||||
}
|
||||
|
||||
export default memo(QuickAddInline);
|
||||
function QuickAddInline({ previousEventId, parentBlock }: QuickAddInlineProps) {
|
||||
function QuickAddInline({ previousEventId, parentGroup }: QuickAddInlineProps) {
|
||||
const { addEntry } = useEntryActions();
|
||||
|
||||
const addEvent = () => {
|
||||
addEntry(
|
||||
{
|
||||
type: SupportedEntry.Event,
|
||||
parent: parentBlock,
|
||||
parent: parentGroup,
|
||||
},
|
||||
{
|
||||
after: previousEventId,
|
||||
@@ -32,7 +32,7 @@ function QuickAddInline({ previousEventId, parentBlock }: QuickAddInlineProps) {
|
||||
|
||||
const addDelay = () => {
|
||||
addEntry(
|
||||
{ type: SupportedEntry.Delay, parent: parentBlock },
|
||||
{ type: SupportedEntry.Delay, parent: parentGroup },
|
||||
{
|
||||
lastEventId: previousEventId,
|
||||
after: previousEventId,
|
||||
@@ -42,7 +42,7 @@ function QuickAddInline({ previousEventId, parentBlock }: QuickAddInlineProps) {
|
||||
|
||||
const addMilestone = () => {
|
||||
addEntry(
|
||||
{ type: SupportedEntry.Milestone, parent: parentBlock },
|
||||
{ type: SupportedEntry.Milestone, parent: parentGroup },
|
||||
{
|
||||
lastEventId: previousEventId,
|
||||
after: previousEventId,
|
||||
@@ -50,12 +50,12 @@ function QuickAddInline({ previousEventId, parentBlock }: QuickAddInlineProps) {
|
||||
);
|
||||
};
|
||||
|
||||
const addBlock = () => {
|
||||
if (parentBlock !== null) {
|
||||
const addGroup = () => {
|
||||
if (parentGroup !== null) {
|
||||
return;
|
||||
}
|
||||
addEntry(
|
||||
{ type: SupportedEntry.Block },
|
||||
{ type: SupportedEntry.Group },
|
||||
{
|
||||
lastEventId: previousEventId,
|
||||
after: previousEventId,
|
||||
@@ -70,7 +70,7 @@ function QuickAddInline({ previousEventId, parentBlock }: QuickAddInlineProps) {
|
||||
{ type: 'item', icon: IoAdd, label: 'Add Event', onClick: addEvent },
|
||||
{ type: 'item', icon: IoAdd, label: 'Add Delay', onClick: addDelay },
|
||||
{ type: 'item', icon: IoAdd, label: 'Add Milestone', onClick: addMilestone },
|
||||
{ type: 'item', icon: IoAdd, label: 'Add Group', onClick: addBlock, disabled: parentBlock !== null },
|
||||
{ type: 'item', icon: IoAdd, label: 'Add Group', onClick: addGroup, disabled: parentGroup !== null },
|
||||
]}
|
||||
render={<IconButton size='small' variant='primary' className={style.addButton} />}
|
||||
>
|
||||
|
||||
@@ -130,7 +130,7 @@ export default function RundownEvent({
|
||||
}),
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{ type: 'item', label: 'Group', icon: IoFolder, onClick: () => actionHandler('group') },
|
||||
{ type: 'item', label: 'Group', icon: IoFolder, onClick: () => actionHandler('make-group') },
|
||||
{ type: 'divider' },
|
||||
{ type: 'item', label: 'Delete', icon: IoTrash, onClick: () => actionHandler('delete') },
|
||||
]
|
||||
@@ -205,8 +205,8 @@ export default function RundownEvent({
|
||||
}
|
||||
|
||||
const elementInFocus = document.activeElement;
|
||||
// we know the block is the grandparent of our binder
|
||||
const blockElement = handleRef.current.closest('#event-block');
|
||||
// we know the group is the grandparent of our binder
|
||||
const blockElement = handleRef.current.closest('#event-group');
|
||||
|
||||
// we only move focus if the block doesnt already contain focus
|
||||
if (blockElement && !blockElement.contains(elementInFocus)) {
|
||||
|
||||
@@ -14,7 +14,7 @@ import { EndAction, Playback, TimerType, TimeStrategy } from 'ontime-types';
|
||||
|
||||
import Tooltip from '../../../common/components/tooltip/Tooltip';
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
import EditableBlockTitle from '../common/EditableBlockTitle';
|
||||
import TitleEditor from '../common/TitleEditor';
|
||||
import TimeInputFlow from '../time-input-flow/TimeInputFlow';
|
||||
|
||||
import RundownEventChip from './composite/RundownEventChip';
|
||||
@@ -105,7 +105,7 @@ function RundownEventInner({
|
||||
/>
|
||||
</div>
|
||||
<div className={style.titleSection}>
|
||||
<EditableBlockTitle title={title} eventId={eventId} placeholder='Event title' className={style.eventTitle} />
|
||||
<TitleEditor title={title} entryId={eventId} placeholder='Event title' className={style.eventTitle} />
|
||||
{isNext && <span className={style.nextTag}>UP NEXT</span>}
|
||||
</div>
|
||||
<EventBlockPlayback
|
||||
@@ -130,7 +130,7 @@ function RundownEventInner({
|
||||
duration={duration}
|
||||
/>
|
||||
)}
|
||||
<div className={style.statusElements} id='block-status' data-timertype={timerType}>
|
||||
<div className={style.statusElements} id='entry-status' data-timertype={timerType}>
|
||||
<span className={style.eventNote}>{note}</span>
|
||||
<div className={loaded ? style.progressBg : `${style.progressBg} ${style.hidden}`}>
|
||||
{loaded && <EventBlockProgressBar />}
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
@use '../blockMixins' as *;
|
||||
|
||||
.block {
|
||||
.group {
|
||||
@include block-styling;
|
||||
|
||||
margin-block: 0.5rem;
|
||||
@@ -22,7 +22,7 @@
|
||||
.binder {
|
||||
grid-area: binder;
|
||||
height: 100%;
|
||||
background-color: var(--block-color, $gray-1050);
|
||||
background-color: var(--user-bg, $gray-1050);
|
||||
color: $section-white;
|
||||
font-size: 1rem;
|
||||
display: grid;
|
||||
+13
-14
@@ -9,7 +9,7 @@ import {
|
||||
} from 'react-icons/io5';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { EntryId, OntimeBlock } from 'ontime-types';
|
||||
import { EntryId, OntimeGroup } from 'ontime-types';
|
||||
|
||||
import IconButton from '../../../common/components/buttons/IconButton';
|
||||
import { useContextMenu } from '../../../common/hooks/useContextMenu';
|
||||
@@ -17,24 +17,24 @@ import { useEntryActions } from '../../../common/hooks/useEntryAction';
|
||||
import { getOffsetState } from '../../../common/utils/offset';
|
||||
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||
import { formatDuration, formatTime } from '../../../common/utils/time';
|
||||
import EditableBlockTitle from '../common/EditableBlockTitle';
|
||||
import TitleEditor from '../common/TitleEditor';
|
||||
import { canDrop } from '../rundown.utils';
|
||||
import { useEventSelection } from '../useEventSelection';
|
||||
|
||||
import style from './RundownBlock.module.scss';
|
||||
import style from './RundownGroup.module.scss';
|
||||
|
||||
interface RundownBlockProps {
|
||||
data: OntimeBlock;
|
||||
interface RundownGroupProps {
|
||||
data: OntimeGroup;
|
||||
hasCursor: boolean;
|
||||
collapsed: boolean;
|
||||
onCollapse: (collapsed: boolean, groupId: EntryId) => void;
|
||||
}
|
||||
|
||||
//TODO: the block should maybe include a multiple day indicator
|
||||
export default function RundownBlock({ data, hasCursor, collapsed, onCollapse }: RundownBlockProps) {
|
||||
//TODO: the group should maybe include a multiple day indicator
|
||||
export default function RundownGroup({ data, hasCursor, collapsed, onCollapse }: RundownGroupProps) {
|
||||
const handleRef = useRef<null | HTMLSpanElement>(null);
|
||||
const { clone, ungroup, deleteEntry } = useEntryActions();
|
||||
const { selectedEvents, setSelectedBlock } = useEventSelection();
|
||||
const { selectedEvents, setSingleEntrySelection } = useEventSelection();
|
||||
|
||||
const [onContextMenu] = useContextMenu<HTMLDivElement>([
|
||||
{
|
||||
@@ -71,7 +71,7 @@ export default function RundownBlock({ data, hasCursor, collapsed, onCollapse }:
|
||||
} = useSortable({
|
||||
id: data.id,
|
||||
data: {
|
||||
type: 'block',
|
||||
type: 'group',
|
||||
},
|
||||
animateLayoutChanges: () => false,
|
||||
});
|
||||
@@ -87,7 +87,7 @@ export default function RundownBlock({ data, hasCursor, collapsed, onCollapse }:
|
||||
}
|
||||
|
||||
// UI indexes are 1 based
|
||||
setSelectedBlock({ id: data.id });
|
||||
setSingleEntrySelection({ id: data.id });
|
||||
};
|
||||
|
||||
const binderColours = data.colour && getAccessibleColour(data.colour);
|
||||
@@ -115,16 +115,15 @@ export default function RundownBlock({ data, hasCursor, collapsed, onCollapse }:
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx([style.block, hasCursor && style.hasCursor, !collapsed && style.expanded])}
|
||||
className={cx([style.group, hasCursor && style.hasCursor, !collapsed && style.expanded])}
|
||||
ref={setNodeRef}
|
||||
onClick={handleFocusClick}
|
||||
onContextMenu={onContextMenu}
|
||||
style={{
|
||||
// ...(binderColours ? { '--user-bg': binderColours.backgroundColor } : {}),
|
||||
...dragStyle,
|
||||
'--user-bg': data.colour || '#929292',
|
||||
}}
|
||||
data-testid='rundown-block'
|
||||
data-testid='rundown-group'
|
||||
>
|
||||
<div className={style.binder} style={{ ...binderColours }} tabIndex={-1}>
|
||||
<span
|
||||
@@ -138,7 +137,7 @@ export default function RundownBlock({ data, hasCursor, collapsed, onCollapse }:
|
||||
</div>
|
||||
<div className={style.header}>
|
||||
<div className={style.titleRow}>
|
||||
<EditableBlockTitle title={data.title} eventId={data.id} placeholder='Group title' />
|
||||
<TitleEditor title={data.title} entryId={data.id} placeholder='Group title' />
|
||||
<IconButton aria-label='Collapse' variant='subtle-white' onClick={() => onCollapse(!collapsed, data.id)}>
|
||||
{collapsed ? <IoChevronUp /> : <IoChevronDown />}
|
||||
</IconButton>
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
@use '../blockMixins' as *;
|
||||
|
||||
.blockEnd {
|
||||
.groupEnd {
|
||||
cursor: default;
|
||||
height: 1rem;
|
||||
background-color: var(--user-bg, $gray-1050);
|
||||
+6
-6
@@ -1,14 +1,14 @@
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
|
||||
import style from './RundownBlockEnd.module.scss';
|
||||
import style from './RundownGroupEnd.module.scss';
|
||||
|
||||
interface BlockEndProps {
|
||||
interface RundownGroupEndProps {
|
||||
id: string;
|
||||
colour?: string;
|
||||
}
|
||||
|
||||
export default function RundownBlockEnd({ id, colour }: BlockEndProps) {
|
||||
export default function RundownGroupEnd({ id, colour }: RundownGroupEndProps) {
|
||||
const {
|
||||
attributes: dragAttributes,
|
||||
listeners: dragListeners,
|
||||
@@ -18,10 +18,10 @@ export default function RundownBlockEnd({ id, colour }: BlockEndProps) {
|
||||
} = useSortable({
|
||||
id,
|
||||
data: {
|
||||
type: 'end-block',
|
||||
type: 'end-group',
|
||||
},
|
||||
animateLayoutChanges: () => false,
|
||||
disabled: true, // we do not want to drag end blocks
|
||||
disabled: true, // we do not want to drag end groups
|
||||
});
|
||||
|
||||
const dragStyle = {
|
||||
@@ -31,7 +31,7 @@ export default function RundownBlockEnd({ id, colour }: BlockEndProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={style.blockEnd}
|
||||
className={style.groupEnd}
|
||||
ref={setNodeRef}
|
||||
{...dragAttributes}
|
||||
{...dragListeners}
|
||||
@@ -24,7 +24,7 @@ interface RundownMilestoneProps {
|
||||
export default function RundownMilestone({ colour, cue, entryId, hasCursor, title }: RundownMilestoneProps) {
|
||||
const handleRef = useRef<null | HTMLSpanElement>(null);
|
||||
const { updateEntry, deleteEntry } = useEntryActions();
|
||||
const { selectedEvents, setSelectedBlock } = useEventSelection();
|
||||
const { selectedEvents, setSingleEntrySelection } = useEventSelection();
|
||||
|
||||
const [onContextMenu] = useContextMenu<HTMLDivElement>([
|
||||
{
|
||||
@@ -61,7 +61,7 @@ export default function RundownMilestone({ colour, cue, entryId, hasCursor, titl
|
||||
}
|
||||
|
||||
// UI indexes are 1 based
|
||||
setSelectedBlock({ id: entryId });
|
||||
setSingleEntrySelection({ id: entryId });
|
||||
};
|
||||
|
||||
const handleUpdate = (field: 'cue' | 'title', value: string) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
EntryId,
|
||||
isOntimeBlock,
|
||||
isOntimeEvent,
|
||||
isOntimeGroup,
|
||||
isPlayableEvent,
|
||||
MaybeString,
|
||||
OntimeDelay,
|
||||
@@ -81,12 +81,12 @@ function processEntry(
|
||||
processedData.isPast = false;
|
||||
}
|
||||
|
||||
if (isOntimeBlock(entry)) {
|
||||
if (isOntimeGroup(entry)) {
|
||||
processedData.groupId = entry.id;
|
||||
processedData.groupColour = entry.colour;
|
||||
processedData.groupEntries = entry.entries.length;
|
||||
} else {
|
||||
// for delays and blocks, we insert the group metadata
|
||||
// for delays and groups, we insert the group metadata
|
||||
if ((entry as OntimeEvent | OntimeDelay | OntimeMilestone).parent !== processedData.groupId) {
|
||||
// if the parent is not the current group, we need to update the groupId
|
||||
processedData.groupId = (entry as OntimeEvent | OntimeDelay | OntimeMilestone).parent;
|
||||
@@ -129,7 +129,7 @@ function processEntry(
|
||||
* Creates a sortable list of entries
|
||||
* ------------------------------------
|
||||
* Due to limitations in dnd-kit we need to flatten the list of entries
|
||||
* This list should also be aware of any elements that are sortable (ie: block ends)
|
||||
* This list should also be aware of any elements that are sortable (ie: group ends)
|
||||
*/
|
||||
export function makeSortableList(order: EntryId[], entries: RundownEntries): EntryId[] {
|
||||
const flatIds: EntryId[] = [];
|
||||
@@ -141,13 +141,13 @@ export function makeSortableList(order: EntryId[], entries: RundownEntries): Ent
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isOntimeBlock(entry)) {
|
||||
// inside a block there are delays and events
|
||||
if (isOntimeGroup(entry)) {
|
||||
// inside a group there are delays and events
|
||||
// there is no need for special handling
|
||||
flatIds.push(entry.id);
|
||||
flatIds.push(...entry.entries);
|
||||
|
||||
// close the block
|
||||
// close the group
|
||||
flatIds.push(`end-${entry.id}`);
|
||||
} else {
|
||||
flatIds.push(entry.id);
|
||||
@@ -160,14 +160,14 @@ export function makeSortableList(order: EntryId[], entries: RundownEntries): Ent
|
||||
* Checks whether a drop operation is valid
|
||||
* Currently only used for validating dropping groups
|
||||
*/
|
||||
export function canDrop(targetType?: SupportedEntry & 'end-block', targetParent?: EntryId | null): boolean {
|
||||
export function canDrop(targetType?: SupportedEntry & 'end-group', targetParent?: EntryId | null): boolean {
|
||||
// this would mean inserting a group inside another
|
||||
if (targetType === 'end-block') {
|
||||
if (targetType === 'end-group') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// this means swapping places with another group
|
||||
if (targetType === 'block') {
|
||||
if (targetType === 'group') {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ export function canDrop(targetType?: SupportedEntry & 'end-block', targetParent?
|
||||
* - order: How to position relative to the destination:
|
||||
* - 'before': Place before the destination
|
||||
* - 'after': Place after the destination
|
||||
* - 'insert': Insert into the destination (for blocks)
|
||||
* - 'insert': Insert into the destination (for groups)
|
||||
*/
|
||||
export function moveUp(
|
||||
entryId: EntryId,
|
||||
@@ -195,7 +195,7 @@ export function moveUp(
|
||||
|
||||
// 1. moving at the top of the list
|
||||
if (!previousEntryId) {
|
||||
// 1a. we are in a block and need to move outside of it
|
||||
// 1a. we are in a group and need to move outside of it
|
||||
if ('parent' in currentEntry && currentEntry.parent !== null) {
|
||||
return { destinationId: currentEntry.parent, order: 'before' };
|
||||
}
|
||||
@@ -203,9 +203,9 @@ export function moveUp(
|
||||
return { destinationId: null, order: 'before' };
|
||||
}
|
||||
|
||||
// 2. moving a block (always moves at top level)
|
||||
if (isOntimeBlock(currentEntry)) {
|
||||
// 21. if previous entry is inside a block, swap with parent
|
||||
// 2. moving a group (always moves at top level)
|
||||
if (isOntimeGroup(currentEntry)) {
|
||||
// 21. if previous entry is inside a group, swap with parent
|
||||
const previousEntry = entries[previousEntryId];
|
||||
if ('parent' in previousEntry && previousEntry.parent !== null) {
|
||||
return { destinationId: previousEntry.parent, order: 'before' };
|
||||
@@ -218,17 +218,17 @@ export function moveUp(
|
||||
const previousEntry = entries[previousEntryId];
|
||||
const currentEntryParent = currentEntry.parent;
|
||||
|
||||
// 3. moving in and out of a block
|
||||
if (isOntimeBlock(previousEntry)) {
|
||||
// 3a. if we're not already in the block, move into it
|
||||
// 3. moving in and out of a group
|
||||
if (isOntimeGroup(previousEntry)) {
|
||||
// 3a. if we're not already in the group, move into it
|
||||
if (currentEntryParent === null) {
|
||||
return { destinationId: previousEntryId, order: 'insert' };
|
||||
}
|
||||
// 3b. otherwise, move before the block
|
||||
// 3b. otherwise, move before the group
|
||||
return { destinationId: previousEntryId, order: 'before' };
|
||||
}
|
||||
|
||||
// 4. moving into the same block as previous entry
|
||||
// 4. moving into the same group as previous entry
|
||||
if (isOntimeEvent(previousEntry) && previousEntry.parent !== null && currentEntryParent === null) {
|
||||
return { destinationId: previousEntryId, order: 'after' };
|
||||
}
|
||||
@@ -244,7 +244,7 @@ export function moveUp(
|
||||
* - order: How to position relative to the destination:
|
||||
* - 'before': Place before the destination
|
||||
* - 'after': Place after the destination
|
||||
* - 'insert': Insert into the destination (for blocks)
|
||||
* - 'insert': Insert into the destination (for groups)
|
||||
*/
|
||||
export function moveDown(
|
||||
entryId: EntryId,
|
||||
@@ -255,10 +255,10 @@ export function moveDown(
|
||||
const currentIndex = flatOrder.indexOf(entryId);
|
||||
const nextEntryId = flatOrder[currentIndex + 1];
|
||||
|
||||
// 1. check if we're the last entry in a block
|
||||
// 1. check if we're the last entry in a group
|
||||
if ('parent' in currentEntry && currentEntry.parent !== null) {
|
||||
const parentBlock = entries[currentEntry.parent];
|
||||
if (isOntimeBlock(parentBlock) && parentBlock.entries[parentBlock.entries.length - 1] === entryId) {
|
||||
const parentGroup = entries[currentEntry.parent];
|
||||
if (isOntimeGroup(parentGroup) && parentGroup.entries[parentGroup.entries.length - 1] === entryId) {
|
||||
return { destinationId: currentEntry.parent, order: 'after' };
|
||||
}
|
||||
}
|
||||
@@ -268,42 +268,42 @@ export function moveDown(
|
||||
return { destinationId: null, order: 'after' };
|
||||
}
|
||||
|
||||
// 3. moving a block (always moves at top level)
|
||||
if (isOntimeBlock(currentEntry)) {
|
||||
// if next entry is inside this block, skip past all children
|
||||
// 3. moving a group (always moves at top level)
|
||||
if (isOntimeGroup(currentEntry)) {
|
||||
// if next entry is inside this group, skip past all children
|
||||
if (currentEntry.entries.includes(nextEntryId)) {
|
||||
const afterBlockIndex = currentIndex + currentEntry.entries.length + 1;
|
||||
const afterBlockId = flatOrder[afterBlockIndex];
|
||||
const afterGroupIndex = currentIndex + currentEntry.entries.length + 1;
|
||||
const afterGroupId = flatOrder[afterGroupIndex];
|
||||
|
||||
// 2a. block is the last top level entry
|
||||
if (!afterBlockId) {
|
||||
// 2a. group is the last top level entry
|
||||
if (!afterGroupId) {
|
||||
return { destinationId: null, order: 'after' };
|
||||
}
|
||||
// 2b. move after the next top level event
|
||||
return { destinationId: afterBlockId, order: 'after' };
|
||||
return { destinationId: afterGroupId, order: 'after' };
|
||||
}
|
||||
// 2c. empty block move after the next entry
|
||||
// 2c. empty group move after the next entry
|
||||
return { destinationId: nextEntryId, order: 'after' };
|
||||
}
|
||||
|
||||
const nextEntry = entries[nextEntryId];
|
||||
const currentEntryParent = currentEntry.parent;
|
||||
|
||||
// 4. handle moving relative to blocks
|
||||
if (isOntimeBlock(nextEntry)) {
|
||||
// 4. handle moving relative to groups
|
||||
if (isOntimeGroup(nextEntry)) {
|
||||
if (currentEntryParent === null) {
|
||||
// we are entering a block
|
||||
// we are entering a group
|
||||
if (nextEntry.entries.length === 0) {
|
||||
// 3a. if the block is empty, insert into it
|
||||
// 3a. if the group is empty, insert into it
|
||||
return { destinationId: nextEntryId, order: 'insert' };
|
||||
}
|
||||
// 3b. otherwise, add before the first entry in the block
|
||||
const firstBlockEntryId = nextEntry.entries[0];
|
||||
return { destinationId: firstBlockEntryId, order: 'before' };
|
||||
// 3b. otherwise, add before the first entry in the group
|
||||
const firstGroupEntryId = nextEntry.entries[0];
|
||||
return { destinationId: firstGroupEntryId, order: 'before' };
|
||||
}
|
||||
}
|
||||
|
||||
// 5. handle moving between block and top level
|
||||
// 5. handle moving between group and top level
|
||||
const nextEntryParent = isOntimeEvent(nextEntry) ? nextEntry.parent : null;
|
||||
if (nextEntryParent !== null && currentEntryParent === null) {
|
||||
return { destinationId: nextEntryId, order: 'after' };
|
||||
|
||||
@@ -13,7 +13,7 @@ import TimeInputGroup from './TimeInputGroup';
|
||||
|
||||
import style from './TimeInputFlow.module.scss';
|
||||
|
||||
interface EventBlockTimerProps {
|
||||
interface TimeInputFlowProps {
|
||||
eventId: string;
|
||||
countToEnd: boolean;
|
||||
timeStart: number;
|
||||
@@ -36,7 +36,7 @@ function TimeInputFlow({
|
||||
linkStart,
|
||||
delay,
|
||||
showLabels,
|
||||
}: EventBlockTimerProps) {
|
||||
}: TimeInputFlowProps) {
|
||||
const { updateEntry, updateTimer } = useEntryActions();
|
||||
|
||||
// In sync with EventEditorTimes
|
||||
|
||||
@@ -12,8 +12,8 @@ interface EventSelectionStore {
|
||||
selectedEvents: Set<EntryId>;
|
||||
anchoredIndex: MaybeNumber;
|
||||
cursor: MaybeString;
|
||||
entryMode: 'event' | 'block' | null;
|
||||
setSelectedBlock: (selectionArgs: { id: EntryId }) => void;
|
||||
entryMode: 'event' | 'single' | null;
|
||||
setSingleEntrySelection: (selectionArgs: { id: EntryId }) => void;
|
||||
setSelectedEvents: (selectionArgs: { id: EntryId; index: number; selectMode: SelectionMode }) => void;
|
||||
clearSelectedEvents: () => void;
|
||||
clearMultiSelect: () => void;
|
||||
@@ -25,14 +25,14 @@ export const useEventSelection = create<EventSelectionStore>()((set, get) => ({
|
||||
anchoredIndex: null,
|
||||
cursor: null,
|
||||
entryMode: null,
|
||||
setSelectedBlock: ({ id }) => {
|
||||
set({ selectedEvents: new Set([id]), anchoredIndex: null, cursor: id, entryMode: 'block' });
|
||||
setSingleEntrySelection: ({ id }) => {
|
||||
set({ selectedEvents: new Set([id]), anchoredIndex: null, cursor: id, entryMode: 'single' });
|
||||
},
|
||||
setSelectedEvents: ({ id, index, selectMode }) => {
|
||||
const { selectedEvents, anchoredIndex, entryMode } = get();
|
||||
|
||||
// if we are in block mode, we replace the selection and change the mode
|
||||
if (entryMode === 'block') {
|
||||
// if we are in single mode, we replace the selection and change the mode
|
||||
if (entryMode === 'single') {
|
||||
return set({ selectedEvents: new Set([id]), anchoredIndex: index, cursor: id, entryMode: 'event' });
|
||||
}
|
||||
|
||||
|
||||
+16
-16
@@ -2,12 +2,12 @@ import { RefObject, useEffect } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { RowModel, Table } from '@tanstack/react-table';
|
||||
import {
|
||||
isOntimeBlock,
|
||||
isOntimeDelay,
|
||||
isOntimeEvent,
|
||||
isOntimeGroup,
|
||||
isOntimeMilestone,
|
||||
OntimeBlock,
|
||||
OntimeEntry,
|
||||
OntimeGroup,
|
||||
Rundown,
|
||||
} from 'ontime-types';
|
||||
import { colourToHex, cssOrHexToColour } from 'ontime-utils';
|
||||
@@ -18,9 +18,9 @@ import { useSelectedEventId } from '../../../../common/hooks/useSocket';
|
||||
import { getAccessibleColour } from '../../../../common/utils/styleUtils';
|
||||
import { usePersistedCuesheetOptions } from '../../cuesheet.options';
|
||||
|
||||
import BlockRow from './BlockRow';
|
||||
import DelayRow from './DelayRow';
|
||||
import EventRow from './EventRow';
|
||||
import GroupRow from './GroupRow';
|
||||
import MilestoneRow from './MilestoneRow';
|
||||
import { cleanup } from './rowObserver';
|
||||
|
||||
@@ -39,7 +39,7 @@ export default function CuesheetBody({ rowModel, selectedRef, table }: CuesheetB
|
||||
let eventIndex = 0;
|
||||
// for the first event, it will be past if there is something selected
|
||||
let isPast = Boolean(selectedEventId);
|
||||
let hadBlock = false;
|
||||
let hadGroup = false;
|
||||
|
||||
// remove the observer when the table unmounts
|
||||
useEffect(() => {
|
||||
@@ -62,11 +62,11 @@ export default function CuesheetBody({ rowModel, selectedRef, table }: CuesheetB
|
||||
isPast = false;
|
||||
}
|
||||
|
||||
if (isOntimeBlock(entry)) {
|
||||
if (isOntimeGroup(entry)) {
|
||||
return (
|
||||
<BlockRow
|
||||
<GroupRow
|
||||
key={key}
|
||||
blockId={entry.id}
|
||||
groupId={entry.id}
|
||||
colour={entry.colour}
|
||||
hidePast={isPast && hidePast}
|
||||
rowId={row.id}
|
||||
@@ -88,7 +88,7 @@ export default function CuesheetBody({ rowModel, selectedRef, table }: CuesheetB
|
||||
if (entry.parent) {
|
||||
const rundown = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
const parentEntry = rundown?.entries[entry.parent];
|
||||
parentBgColour = (parentEntry as OntimeBlock).colour ?? null;
|
||||
parentBgColour = (parentEntry as OntimeGroup).colour ?? null;
|
||||
}
|
||||
return <DelayRow key={key} duration={delayVal} parentBgColour={parentBgColour} />;
|
||||
}
|
||||
@@ -113,7 +113,7 @@ export default function CuesheetBody({ rowModel, selectedRef, table }: CuesheetB
|
||||
if (entry.parent) {
|
||||
const rundown = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
const parentEntry = rundown?.entries[entry.parent];
|
||||
parentBgColour = (parentEntry as OntimeBlock | undefined)?.colour ?? null;
|
||||
parentBgColour = (parentEntry as OntimeGroup | undefined)?.colour ?? null;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -153,15 +153,15 @@ export default function CuesheetBody({ rowModel, selectedRef, table }: CuesheetB
|
||||
}
|
||||
|
||||
let parentBgColour: string | undefined;
|
||||
let firstAfterBlock = false;
|
||||
let firstAfterGroup = false;
|
||||
if (entry.parent) {
|
||||
const rundown = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
const parentEntry = rundown?.entries[entry.parent] as OntimeBlock | undefined;
|
||||
const parentEntry = rundown?.entries[entry.parent] as OntimeGroup | undefined;
|
||||
parentBgColour = parentEntry?.colour;
|
||||
hadBlock = true;
|
||||
} else if (hadBlock) {
|
||||
firstAfterBlock = true;
|
||||
hadBlock = false;
|
||||
hadGroup = true;
|
||||
} else if (hadGroup) {
|
||||
firstAfterGroup = true;
|
||||
hadGroup = false;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -176,7 +176,7 @@ export default function CuesheetBody({ rowModel, selectedRef, table }: CuesheetB
|
||||
rowBgColour={rowBgColour}
|
||||
parentBgColour={parentBgColour}
|
||||
table={table}
|
||||
firstAfterBlock={firstAfterBlock}
|
||||
firstAfterGroup={firstAfterGroup}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
background: color-mix(in srgb, transparent 80%, var(--user-bg, $gray-500) 20%);
|
||||
}
|
||||
|
||||
&.firstAfterBlock {
|
||||
&.firstAfterGroup {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ interface EventRowProps {
|
||||
rowBgColour?: string;
|
||||
parentBgColour?: string;
|
||||
table: Table<OntimeEntry>;
|
||||
firstAfterBlock: boolean;
|
||||
firstAfterGroup: boolean;
|
||||
}
|
||||
|
||||
export default function EventRow({
|
||||
@@ -39,7 +39,7 @@ export default function EventRow({
|
||||
rowBgColour,
|
||||
parentBgColour,
|
||||
table,
|
||||
firstAfterBlock,
|
||||
firstAfterGroup,
|
||||
}: EventRowProps) {
|
||||
const { cuesheetMode, hideIndexColumn } = table.options.meta?.options ?? {
|
||||
cuesheetMode: AppMode.Edit,
|
||||
@@ -75,7 +75,7 @@ export default function EventRow({
|
||||
className={cx([
|
||||
style.eventRow,
|
||||
event.skip && style.skip,
|
||||
firstAfterBlock && style.firstAfterBlock,
|
||||
firstAfterGroup && style.firstAfterGroup,
|
||||
Boolean(parentBgColour) && style.hasParent,
|
||||
])}
|
||||
style={{
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
@import '../CuesheetTable.module.scss';
|
||||
|
||||
.blockRow {
|
||||
.groupRow {
|
||||
margin-top: 1rem;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
+9
-9
@@ -3,14 +3,14 @@ import { flexRender, Table } from '@tanstack/react-table';
|
||||
import { EntryId, OntimeEntry, SupportedEntry } from 'ontime-types';
|
||||
|
||||
import IconButton from '../../../../common/components/buttons/IconButton';
|
||||
import { useCurrentBlockId } from '../../../../common/hooks/useSocket';
|
||||
import { useCurrentGroupId } from '../../../../common/hooks/useSocket';
|
||||
import { AppMode } from '../../../../ontimeConfig';
|
||||
import { useCuesheetTableMenu } from '../cuesheet-table-menu/useCuesheetTableMenu';
|
||||
|
||||
import style from './BlockRow.module.scss';
|
||||
import style from './GroupRow.module.scss';
|
||||
|
||||
interface BlockRowProps {
|
||||
blockId: EntryId;
|
||||
interface GroupRowProps {
|
||||
groupId: EntryId;
|
||||
colour: string;
|
||||
hidePast: boolean;
|
||||
rowId: string;
|
||||
@@ -18,8 +18,8 @@ interface BlockRowProps {
|
||||
table: Table<OntimeEntry>;
|
||||
}
|
||||
|
||||
export default function BlockRow({ blockId, colour, hidePast, rowId, rowIndex, table }: BlockRowProps) {
|
||||
const { currentBlockId } = useCurrentBlockId();
|
||||
export default function GroupRow({ groupId, colour, hidePast, rowId, rowIndex, table }: GroupRowProps) {
|
||||
const { currentGroupId } = useCurrentGroupId();
|
||||
|
||||
const { cuesheetMode, hideIndexColumn } = table.options.meta?.options ?? {
|
||||
cuesheetMode: AppMode.Edit,
|
||||
@@ -28,12 +28,12 @@ export default function BlockRow({ blockId, colour, hidePast, rowId, rowIndex, t
|
||||
|
||||
const openMenu = useCuesheetTableMenu((store) => store.openMenu);
|
||||
|
||||
if (hidePast && !currentBlockId) {
|
||||
if (hidePast && !currentGroupId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr className={style.blockRow} style={{ '--user-bg': colour }} data-testid='cuesheet-block'>
|
||||
<tr className={style.groupRow} style={{ '--user-bg': colour }} data-testid='cuesheet-group'>
|
||||
{cuesheetMode === AppMode.Edit && (
|
||||
<td className={style.actionColumn} tabIndex={-1} role='cell'>
|
||||
<IconButton
|
||||
@@ -43,7 +43,7 @@ export default function BlockRow({ blockId, colour, hidePast, rowId, rowIndex, t
|
||||
onClick={(e) => {
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const yPos = 8 + rect.y + rect.height / 2;
|
||||
openMenu({ x: rect.x, y: yPos }, blockId, SupportedEntry.Block, rowIndex, null, null);
|
||||
openMenu({ x: rect.x, y: yPos }, groupId, SupportedEntry.Group, rowIndex, null, null);
|
||||
}}
|
||||
>
|
||||
<IoEllipsisHorizontal />
|
||||
+2
-2
@@ -134,7 +134,7 @@ function MakeMultiLineField({ row, column, table }: CellContext<OntimeEntry, unk
|
||||
[column.id, row.index, table.options.meta],
|
||||
);
|
||||
|
||||
// not all entries have all properties (eg blocks)
|
||||
// not all entries have all properties (eg groups)
|
||||
const initialValue = row.original[column.id as keyof OntimeEntry];
|
||||
if (initialValue === undefined) {
|
||||
return null;
|
||||
@@ -174,7 +174,7 @@ function MakeSingleLineField({ row, column, table }: CellContext<OntimeEntry, un
|
||||
[column.id, row.index, table.options.meta],
|
||||
);
|
||||
|
||||
// not all entries have all properties (eg blocks)
|
||||
// not all entries have all properties (eg groups)
|
||||
const initialValue = row.original[column.id as keyof OntimeEntry];
|
||||
if (initialValue === undefined) {
|
||||
return null;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useSessionStorage } from '@mantine/hooks';
|
||||
import { EntryId, isOntimeBlock, isOntimeEvent, MaybeString, SupportedEntry } from 'ontime-types';
|
||||
import { EntryId, isOntimeEvent, isOntimeGroup, MaybeString, SupportedEntry } from 'ontime-types';
|
||||
|
||||
import { useFlatRundown } from '../../../common/hooks-query/useRundown';
|
||||
import { useEventSelection } from '../../../features/rundown/useEventSelection';
|
||||
|
||||
const maxResults = 12;
|
||||
|
||||
type FilterableBlock = {
|
||||
type: SupportedEntry.Block;
|
||||
type FilterableGroup = {
|
||||
type: SupportedEntry.Group;
|
||||
id: string;
|
||||
index: number;
|
||||
title: string;
|
||||
@@ -25,7 +25,7 @@ type FilterableEvent = {
|
||||
parent: MaybeString;
|
||||
};
|
||||
|
||||
type FilterableEntry = FilterableBlock | FilterableEvent;
|
||||
type FilterableEntry = FilterableGroup | FilterableEvent;
|
||||
|
||||
export default function useFinder() {
|
||||
const { data, rundownId } = useFlatRundown();
|
||||
@@ -179,15 +179,15 @@ export default function useFinder() {
|
||||
}
|
||||
eventIndex++;
|
||||
}
|
||||
if (isOntimeBlock(event)) {
|
||||
if (isOntimeGroup(event)) {
|
||||
if (event.title.toLowerCase().includes(searchString)) {
|
||||
remaining--;
|
||||
results.push({
|
||||
type: SupportedEntry.Block,
|
||||
type: SupportedEntry.Group,
|
||||
id: event.id,
|
||||
index: i,
|
||||
title: event.title,
|
||||
} satisfies FilterableBlock);
|
||||
} satisfies FilterableGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user