mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-15 12:23:51 +00:00
V2 monorepo (#285)
* refactor(project structure): UI * refactor(project structure): extract utilities * refactor(project structure): remove unused * refactor(project structure): electron * refactor(project structure): server refactor: migrate to vitest refactor: monorepo config * refactor: extract application menu * refactor: exit process * refactor: extract tray menu * chore: electron build * Added Seconds in studio clock #282 --------- Co-authored-by: Fabian Posenau <fabian@fphome.de> --------- Co-authored-by: Fabian Posenau <fabian.p99@gmx.de> Co-authored-by: Fabian Posenau <fabian@fphome.de>
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
import { useCallback, useContext, useEffect, useMemo } from 'react';
|
||||
import { useBlockLayout, useColumnOrder, useResizeColumns, useTable } from 'react-table';
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
TouchSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
horizontalListSortingStrategy,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
} from '@dnd-kit/sortable';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { TableSettingsContext } from '../../common/context/TableSettingsContext';
|
||||
import { useLocalStorage } from '../../common/hooks/useLocalStorage';
|
||||
import { tooltipDelayFast } from '../../ontimeConfig';
|
||||
|
||||
import SortableCell from './tableElements/SortableCell';
|
||||
import TableSettings from './tableElements/TableSettings';
|
||||
import BlockRow from './tableRows/BlockRow';
|
||||
import DelayRow from './tableRows/DelayRow';
|
||||
import EventRow from './tableRows/EventRow';
|
||||
import { makeColumns } from './columns';
|
||||
import { defaultColumnOrder, defaultHiddenColumns } from './defaults';
|
||||
|
||||
import style from './Table.module.scss';
|
||||
|
||||
export default function OntimeTable({ tableData, userFields, selectedId, handleUpdate }) {
|
||||
const { followSelected, showSettings } = useContext(TableSettingsContext);
|
||||
const [columnOrder, saveColumnOrder] = useLocalStorage('table-order', defaultColumnOrder);
|
||||
const [columnSize, saveColumnSize] = useLocalStorage('table-sizes', {});
|
||||
const [hiddenColumns, saveHiddenColumns] = useLocalStorage('table-hidden', defaultHiddenColumns);
|
||||
const columns = useMemo(() => makeColumns(columnSize, userFields), [columnSize, userFields]);
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
rows,
|
||||
prepareRow,
|
||||
setColumnOrder,
|
||||
allColumns,
|
||||
setHiddenColumns,
|
||||
toggleHideAllColumns,
|
||||
state,
|
||||
} = useTable(
|
||||
{
|
||||
columns,
|
||||
data: tableData,
|
||||
initialState: {
|
||||
hiddenColumns,
|
||||
},
|
||||
handleUpdate,
|
||||
},
|
||||
useColumnOrder,
|
||||
useBlockLayout,
|
||||
useResizeColumns
|
||||
);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
delay: 100,
|
||||
tolerance: 50,
|
||||
},
|
||||
}),
|
||||
useSensor(TouchSensor, {
|
||||
activationConstraint: {
|
||||
delay: 100,
|
||||
tolerance: 50,
|
||||
},
|
||||
}),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
);
|
||||
|
||||
const handleResetReordering = useCallback(() => {
|
||||
saveColumnOrder(defaultColumnOrder);
|
||||
setColumnOrder(defaultColumnOrder);
|
||||
}, [saveColumnOrder, setColumnOrder]);
|
||||
|
||||
const handleResetResizing = useCallback(() => {
|
||||
saveColumnSize({});
|
||||
}, [saveColumnSize]);
|
||||
|
||||
const handleResetToggles = useCallback(() => {
|
||||
setHiddenColumns(defaultHiddenColumns);
|
||||
saveHiddenColumns(defaultHiddenColumns);
|
||||
}, [saveHiddenColumns, setHiddenColumns]);
|
||||
|
||||
const clearToggles = useCallback(() => {
|
||||
toggleHideAllColumns(false);
|
||||
saveHiddenColumns([]);
|
||||
}, [saveHiddenColumns, toggleHideAllColumns]);
|
||||
|
||||
const handleOnDragEnd = useCallback((event) => {
|
||||
const { delta, active, over } = event;
|
||||
|
||||
// cancel if delta y is greater than 200
|
||||
if (delta.y > 200) return;
|
||||
|
||||
const cols = [...columnOrder];
|
||||
|
||||
// get index of from
|
||||
const fromIndex = cols.findIndex((i) => i === active.id);
|
||||
|
||||
// get index of to
|
||||
const toIndex = cols.findIndex((i) => i === over.id);
|
||||
|
||||
if (toIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// reorder
|
||||
const [reorderedItem] = cols.splice(fromIndex, 1);
|
||||
cols.splice(toIndex, 0, reorderedItem);
|
||||
|
||||
saveColumnOrder(cols);
|
||||
setColumnOrder(cols);
|
||||
}, [columnOrder, saveColumnOrder, setColumnOrder]);
|
||||
|
||||
// save hidden columns object to local storage
|
||||
useEffect(() => {
|
||||
saveHiddenColumns(state.hiddenColumns);
|
||||
}, [saveHiddenColumns, state.hiddenColumns]);
|
||||
|
||||
// save column sizes to local storage
|
||||
useEffect(() => {
|
||||
// property changes from title of column to null on resize end
|
||||
if (state.columnResizing?.isResizingColumn !== null) {
|
||||
return;
|
||||
}
|
||||
const cols = state.columnResizing.columnWidths;
|
||||
saveColumnSize((prev) => ({ ...prev, ...cols }));
|
||||
}, [saveColumnSize, state.columnResizing]);
|
||||
|
||||
// scroll to active cue
|
||||
useEffect(() => {
|
||||
if (followSelected) {
|
||||
const el = document.getElementById(selectedId);
|
||||
if (el) {
|
||||
el.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'nearest',
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [followSelected, selectedId]);
|
||||
|
||||
// keep order of events
|
||||
let eventIndex = 0;
|
||||
// keep delay (ms)
|
||||
let cumulativeDelay = 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
{showSettings && (
|
||||
<TableSettings
|
||||
columns={allColumns}
|
||||
handleResetResizing={handleResetResizing}
|
||||
handleResetReordering={handleResetReordering}
|
||||
handleResetToggles={handleResetToggles}
|
||||
handleClearToggles={clearToggles}
|
||||
/>
|
||||
)}
|
||||
<table {...getTableProps()} className={style.ontimeTable}>
|
||||
<thead className={style.tableHeader}>
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key, ...restHeaderGroupProps } = headerGroup.getHeaderGroupProps();
|
||||
return (
|
||||
<DndContext
|
||||
key={key}
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleOnDragEnd}
|
||||
>
|
||||
<tr {...restHeaderGroupProps}>
|
||||
<th className={style.indexColumn}>
|
||||
<Tooltip label='Event Order' openDelay={tooltipDelayFast}>
|
||||
#
|
||||
</Tooltip>
|
||||
</th>
|
||||
<SortableContext
|
||||
key={key}
|
||||
items={headerGroup.headers}
|
||||
strategy={horizontalListSortingStrategy}
|
||||
>
|
||||
{headerGroup.headers.map((column) => {
|
||||
const { key } = column.getHeaderProps();
|
||||
return <SortableCell key={key} column={column} />;
|
||||
})}
|
||||
</SortableContext>
|
||||
</tr>
|
||||
</DndContext>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody {...getTableBodyProps} className={style.tableBody}>
|
||||
{/*This is saving in place of a default component*/}
|
||||
{/* eslint-disable-next-line array-callback-return */}
|
||||
{rows.map((row) => {
|
||||
prepareRow(row);
|
||||
const { key } = row.getRowProps();
|
||||
const type = row.original.type;
|
||||
if (type === 'event') {
|
||||
eventIndex++;
|
||||
return (
|
||||
<EventRow
|
||||
key={key}
|
||||
row={row}
|
||||
index={eventIndex}
|
||||
selectedId={selectedId}
|
||||
delay={cumulativeDelay}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (type === 'delay') {
|
||||
if (row.original.duration != null) {
|
||||
cumulativeDelay += row.original.duration;
|
||||
}
|
||||
return <DelayRow key={key} row={row} />;
|
||||
}
|
||||
if (type === 'block') {
|
||||
cumulativeDelay = 0;
|
||||
return <BlockRow key={key} row={row} />;
|
||||
}
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
OntimeTable.propTypes = {
|
||||
tableData: PropTypes.array,
|
||||
userFields: PropTypes.object,
|
||||
handleUpdate: PropTypes.func.isRequired,
|
||||
selectedId: PropTypes.string,
|
||||
showSettings: PropTypes.bool,
|
||||
followSelected: PropTypes.bool,
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import ProtectRoute from '../../common/components/protect-route/ProtectRoute';
|
||||
import { TableSettingsProvider } from '../../common/context/TableSettingsContext';
|
||||
|
||||
import TableWrapper from './TableWrapper';
|
||||
|
||||
export default function ProtectedTable() {
|
||||
return (
|
||||
<ProtectRoute>
|
||||
<TableSettingsProvider>
|
||||
<TableWrapper />
|
||||
</TableSettingsProvider>
|
||||
</ProtectRoute>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
@use '../../theme/main' as *;
|
||||
|
||||
.tableWrapper,
|
||||
.tableWrapper__dark {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-size: 16px;
|
||||
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
padding: 2rem;
|
||||
display: grid;
|
||||
|
||||
grid-template-columns: calc(100vw - 4rem);
|
||||
grid-template-rows: auto auto 1fr;
|
||||
grid-template-areas:
|
||||
'header'
|
||||
'settings'
|
||||
'table';
|
||||
gap: 1rem;
|
||||
overflow: scroll;
|
||||
|
||||
& > * {
|
||||
width: 100%;
|
||||
border: 1px solid $ontime-pink;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.header {
|
||||
grid-area: header;
|
||||
display: grid;
|
||||
height: max-content;
|
||||
grid-template-areas:
|
||||
'name playback running time actions'
|
||||
'now playback running time actions';
|
||||
grid-template-columns: 1fr auto 10em 12.5em auto;
|
||||
align-items: center;
|
||||
padding: 0.25em 1em;
|
||||
|
||||
.headerName {
|
||||
grid-area: name;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.headerName:after {
|
||||
content: '\200b';
|
||||
}
|
||||
|
||||
.headerNow {
|
||||
grid-area: now;
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
.headerNow:after {
|
||||
content: '\200b';
|
||||
}
|
||||
|
||||
.headerPlayback {
|
||||
grid-area: playback;
|
||||
color: $ontime-pink;
|
||||
text-align: center;
|
||||
|
||||
svg {
|
||||
font-size: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
.headerRunning {
|
||||
grid-area: running;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.headerClock {
|
||||
grid-area: time;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.headerActions {
|
||||
grid-area: actions;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 1.5em;
|
||||
padding-left: 10vw;
|
||||
color: darken($ontime-pink, 8%);
|
||||
}
|
||||
}
|
||||
|
||||
.tableSettings {
|
||||
grid-area: settings;
|
||||
padding: 1rem;
|
||||
|
||||
.options,
|
||||
.buttonRow {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.options {
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
.buttonRow {
|
||||
padding-top: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
.ontimeTable {
|
||||
grid-area: table;
|
||||
|
||||
border-collapse: separate;
|
||||
border-spacing: 16px;
|
||||
padding: 1rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
th, td {
|
||||
touch-action: auto;
|
||||
padding: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
td {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tableHeader {
|
||||
position: sticky;
|
||||
top: -1em;
|
||||
z-index: 10;
|
||||
|
||||
th {
|
||||
font-size: 0.9em;
|
||||
font-weight: 200;
|
||||
text-align: left;
|
||||
|
||||
.resizer {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transform: translateX(50%);
|
||||
z-index: 1;
|
||||
touch-action: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tableBody {
|
||||
tr {
|
||||
td:hover {
|
||||
background-color: lighten($ontime-pink, 10%) !important;
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
.selected > td {
|
||||
background-color: rgba($ontime-accent, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.indexColumn {
|
||||
font-weight: 200;
|
||||
text-align: right;
|
||||
width: 2.5em;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.blockCell,
|
||||
.delayCell {
|
||||
width: 100%;
|
||||
font-weight: 400;
|
||||
font-size: 0.9em;
|
||||
text-align: center;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.delayCell {
|
||||
background-color: $block-delay-color;
|
||||
}
|
||||
|
||||
.blockCell {
|
||||
background-color: $block-block-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$bg-theme-light: #fcfcfc;
|
||||
$cell-theme-light: #ececec;
|
||||
$text-theme-light: #202020;
|
||||
$bg-theme-dark: #121212;
|
||||
$bg2-theme-dark: #1c1c1c;
|
||||
$cell-theme-dark: #2d2d2d;
|
||||
$text-theme-dark: white;
|
||||
|
||||
.tableWrapper {
|
||||
background-color: $bg-theme-light;
|
||||
color: black;
|
||||
|
||||
* {
|
||||
background-color: $bg-theme-light;
|
||||
scrollbar-color: $ontime-pink rgba(0, 0, 0, 0.13);
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
background-color: rgba(0, 0, 0, 0.07);
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: lighten($ontime-pink, 15%);
|
||||
}
|
||||
|
||||
td {
|
||||
background-color: $cell-theme-light;
|
||||
border: 1px solid $bg-theme-light;
|
||||
color: #121212;
|
||||
}
|
||||
.actionText:hover,
|
||||
.actionIcon:hover,
|
||||
.actionDisabled:hover {
|
||||
color: black;
|
||||
transition: 300ms;
|
||||
}
|
||||
}
|
||||
|
||||
.tableWrapper__dark {
|
||||
background-color: $bg-theme-dark;
|
||||
color: white;
|
||||
|
||||
* {
|
||||
background-color: $bg-theme-dark;
|
||||
scrollbar-color: $ontime-pink rgba(255, 255, 255, 0.13);
|
||||
}
|
||||
|
||||
td {
|
||||
background-color: $cell-theme-dark;
|
||||
border: 1px solid $bg-theme-dark;
|
||||
color: #ececec;
|
||||
}
|
||||
.actionText:hover,
|
||||
.actionIcon:hover,
|
||||
.actionDisabled:hover {
|
||||
color: white;
|
||||
transition: 300ms;
|
||||
}
|
||||
}
|
||||
|
||||
.timer {
|
||||
font-size: 1.7em;
|
||||
letter-spacing: 1px;
|
||||
line-height: 1.2em;
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: $ontime-pink;
|
||||
font-size: 0.8em;
|
||||
font-weight: 200;
|
||||
line-height: 0.75em;
|
||||
}
|
||||
|
||||
svg {
|
||||
background-color: inherit !important;
|
||||
}
|
||||
|
||||
@mixin action-element() {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.actionIcon {
|
||||
@include action-element();
|
||||
}
|
||||
|
||||
.actionText {
|
||||
@include action-element();
|
||||
font-size: 0.65em;
|
||||
}
|
||||
|
||||
.actionDisabled {
|
||||
@include action-element();
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.dragging {
|
||||
border: 1px solid $ontime-pink;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.check {
|
||||
font-size: 1.5em;
|
||||
background-color: transparent !important;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@keyframes rotation {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { useContext } from 'react';
|
||||
import { Divider, Tooltip } from '@chakra-ui/react';
|
||||
import { FiSettings } from '@react-icons/all-files/fi/FiSettings';
|
||||
import { FiTarget } from '@react-icons/all-files/fi/FiTarget';
|
||||
import { IoContract } from '@react-icons/all-files/io5/IoContract';
|
||||
import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
|
||||
import { IoMoon } from '@react-icons/all-files/io5/IoMoon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { TableSettingsContext } from '../../common/context/TableSettingsContext';
|
||||
import useFullscreen from '../../common/hooks/useFullscreen';
|
||||
import { useTimer } from '../../common/hooks/useSocket';
|
||||
import useEvent from '../../common/hooks-query/useEvent';
|
||||
import { formatDisplay, millisToSeconds } from '../../common/utils/dateConfig';
|
||||
import { formatTime } from '../../common/utils/time';
|
||||
import { tooltipDelayFast } from '../../ontimeConfig';
|
||||
|
||||
import PlaybackIcon from './tableElements/PlaybackIcon';
|
||||
|
||||
import style from './Table.module.scss';
|
||||
|
||||
export default function TableHeader({ handleCSVExport, featureData }) {
|
||||
const { followSelected, showSettings, toggleTheme, toggleSettings, toggleFollow } =
|
||||
useContext(TableSettingsContext);
|
||||
const { data: timer } = useTimer();
|
||||
const { isFullScreen, toggleFullScreen } = useFullscreen();
|
||||
const { data: event } = useEvent();
|
||||
|
||||
const selected = !featureData.numEvents
|
||||
? 'No events'
|
||||
: `Event ${featureData.selectedEventIndex != null ? featureData.selectedEventIndex + 1 : '-'}/${
|
||||
featureData.numEvents ? featureData.numEvents : '-'
|
||||
}`;
|
||||
|
||||
// prepare presentation variables
|
||||
const isOvertime = timer.current < 0;
|
||||
const timerNow = `${isOvertime ? '-' : ''}${formatDisplay(millisToSeconds(timer.current))}`;
|
||||
const timeNow = formatTime(timer.clock, {
|
||||
showSeconds: true,
|
||||
format: 'hh:mm:ss a',
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={style.header}>
|
||||
<div className={style.headerName}>{event?.title || ''}</div>
|
||||
<div className={style.headerNow}>{featureData.titleNow}</div>
|
||||
<div className={style.headerPlayback}>
|
||||
<span className={style.label}>{selected}</span>
|
||||
<br />
|
||||
<PlaybackIcon state={featureData.playback} />
|
||||
</div>
|
||||
<div className={style.headerRunning}>
|
||||
<span className={style.label}>Running Timer</span>
|
||||
<br />
|
||||
<span className={style.timer}>{timerNow}</span>
|
||||
</div>
|
||||
<div className={style.headerClock}>
|
||||
<span className={style.label}>Time Now</span>
|
||||
<br />
|
||||
<span className={style.timer}>{timeNow}</span>
|
||||
</div>
|
||||
<div className={style.headerActions}>
|
||||
<Tooltip openDelay={tooltipDelayFast} label='Follow selected'>
|
||||
<span className={followSelected ? style.actionIcon : style.actionDisabled}>
|
||||
<FiTarget onClick={() => toggleFollow()} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip openDelay={tooltipDelayFast} label='Show settings'>
|
||||
<span className={showSettings ? style.actionIcon : style.actionDisabled}>
|
||||
<FiSettings onClick={() => toggleSettings()} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip openDelay={tooltipDelayFast} label='Toggle dark mode'>
|
||||
<span className={style.actionIcon}>
|
||||
<IoMoon onClick={() => toggleTheme()} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip openDelay={tooltipDelayFast} label='Toggle Fullscreen'>
|
||||
<span className={style.actionIcon}>
|
||||
{isFullScreen ? (
|
||||
<IoContract onClick={() => toggleFullScreen()} />
|
||||
) : (
|
||||
<IoExpand onClick={() => toggleFullScreen()} />
|
||||
)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Divider />
|
||||
<Tooltip openDelay={tooltipDelayFast} label='Export to CSV'>
|
||||
<span className={style.actionText} onClick={() => handleCSVExport(event)}>
|
||||
CSV
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TableHeader.propTypes = {
|
||||
handleCSVExport: PropTypes.func.isRequired,
|
||||
featureData: PropTypes.shape({
|
||||
playback: PropTypes.string,
|
||||
selectedEventId: PropTypes.string,
|
||||
selectedEventIndex: PropTypes.number,
|
||||
numEvents: PropTypes.number,
|
||||
titleNow: PropTypes.string,
|
||||
}),
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
import { useCallback, useContext, useEffect } from 'react';
|
||||
|
||||
import { TableSettingsContext } from '../../common/context/TableSettingsContext';
|
||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||
import { useCuesheet } from '../../common/hooks/useSocket';
|
||||
import useRundown from '../../common/hooks-query/useRundown';
|
||||
import useUserFields from '../../common/hooks-query/useUserFields';
|
||||
|
||||
import OntimeTable from './OntimeTable';
|
||||
import TableHeader from './TableHeader';
|
||||
import { makeCSV, makeTable } from './utils';
|
||||
|
||||
import style from './Table.module.scss';
|
||||
|
||||
export default function TableWrapper() {
|
||||
const { data: rundown } = useRundown();
|
||||
const { data: userFields } = useUserFields();
|
||||
const { data: featureData } = useCuesheet();
|
||||
const { updateEvent } = useEventAction();
|
||||
const { theme } = useContext(TableSettingsContext);
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
document.title = 'ontime - Cuesheet';
|
||||
}, []);
|
||||
|
||||
const handleUpdate = useCallback(
|
||||
async (rowIndex, accessor, payload) => {
|
||||
if (rowIndex == null || accessor == null || payload == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if value is the same
|
||||
const event = rundown[rowIndex];
|
||||
if (event == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event[accessor] === payload) {
|
||||
return;
|
||||
}
|
||||
// check if value is valid
|
||||
// as of now, the fields do not have any validation
|
||||
if (typeof payload !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
// cleanup
|
||||
const cleanVal = payload.trim();
|
||||
const mutationObject = {
|
||||
id: event.id,
|
||||
[accessor]: cleanVal,
|
||||
};
|
||||
|
||||
// submit
|
||||
try {
|
||||
await updateEvent(mutationObject);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, [updateEvent, rundown]);
|
||||
|
||||
const exportHandler = useCallback(
|
||||
(headerData) => {
|
||||
if (!headerData || !rundown || !userFields) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sheetData = makeTable(headerData, rundown, userFields);
|
||||
const csvContent = makeCSV(sheetData);
|
||||
const encodedUri = encodeURI(csvContent);
|
||||
const link = document.createElement('a');
|
||||
link.setAttribute('href', encodedUri);
|
||||
link.setAttribute('download', 'ontime export.csv');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
},
|
||||
[rundown, userFields]
|
||||
);
|
||||
|
||||
if (typeof rundown === 'undefined' || typeof userFields === 'undefined') {
|
||||
return <span>loading...</span>;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={theme === 'dark' ? style.tableWrapper__dark : style.tableWrapper}
|
||||
data-testid="cuesheet"
|
||||
>
|
||||
<TableHeader handleCSVExport={exportHandler} featureData={featureData} />
|
||||
<OntimeTable
|
||||
tableData={rundown}
|
||||
userFields={userFields}
|
||||
handleUpdate={handleUpdate}
|
||||
selectedId={featureData.selectedEventId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Vitest Snapshot v1
|
||||
|
||||
exports[`makeTable() > returns array of arrays with given fields 1`] = `
|
||||
[
|
||||
[
|
||||
"Ontime · Schedule Template",
|
||||
],
|
||||
[
|
||||
"Event Name",
|
||||
"",
|
||||
],
|
||||
[
|
||||
"Event URL",
|
||||
"",
|
||||
],
|
||||
[],
|
||||
[
|
||||
"Time Start",
|
||||
"Time End",
|
||||
"Event Title",
|
||||
"Presenter Name",
|
||||
"Event Subtitle",
|
||||
"Is Public? (x)",
|
||||
"Notes",
|
||||
"Colour",
|
||||
"user0:test",
|
||||
],
|
||||
[
|
||||
"00:00:00",
|
||||
"00:00:00",
|
||||
"test title 1",
|
||||
"",
|
||||
"",
|
||||
"x",
|
||||
"",
|
||||
"",
|
||||
"test",
|
||||
"test",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`makeTable() returns array of arrays with given fields 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
"Ontime · Schedule Template",
|
||||
],
|
||||
Array [
|
||||
"Event Name",
|
||||
"",
|
||||
],
|
||||
Array [
|
||||
"Event URL",
|
||||
"",
|
||||
],
|
||||
Array [],
|
||||
Array [
|
||||
"Time Start",
|
||||
"Time End",
|
||||
"Event Title",
|
||||
"Presenter Name",
|
||||
"Event Subtitle",
|
||||
"Is Public? (x)",
|
||||
"Notes",
|
||||
"Colour",
|
||||
"user0:test",
|
||||
],
|
||||
Array [
|
||||
"00:00:00",
|
||||
"00:00:00",
|
||||
"test title 1",
|
||||
"",
|
||||
"",
|
||||
"x",
|
||||
"",
|
||||
"",
|
||||
"test",
|
||||
"test",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
],
|
||||
]
|
||||
`;
|
||||
@@ -0,0 +1,93 @@
|
||||
import { makeCSV, makeTable, parseField } from '../utils';
|
||||
|
||||
describe('parseField()', () => {
|
||||
it('returns a string from given millis on timeStart and TimeEnd', () => {
|
||||
const testData1 = 1000;
|
||||
const testData2 = 60000;
|
||||
expect(parseField('timeStart', testData1)).toBe('00:00:01');
|
||||
expect(parseField('timeEnd', testData2)).toBe('00:01:00');
|
||||
expect(parseField('timeEnd', testData2)).toBe('00:01:00');
|
||||
});
|
||||
|
||||
describe('returns an x when isPublic is truthy, empty string otherwise', () => {
|
||||
const testTruthy = [1, true, 'x', 'test'];
|
||||
const testFalsy = ['', null, undefined, false, 0];
|
||||
|
||||
testTruthy.forEach((value) => {
|
||||
test(`${value}`, () => {
|
||||
expect(parseField('isPublic', value)).toBe('x');
|
||||
});
|
||||
});
|
||||
testFalsy.forEach((value) => {
|
||||
test(`${value}`, () => {
|
||||
expect(parseField('isPublic', value)).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an empty string on undefined fields', () => {
|
||||
expect(parseField('presenter', undefined)).toBe('');
|
||||
});
|
||||
|
||||
describe('simply returns any other value in any other field', () => {
|
||||
const testFields = [
|
||||
{ field: 'nothing', value: 123 },
|
||||
{ field: 'title', value: 'test' },
|
||||
{ field: 'presenter', value: 'test' },
|
||||
{ field: 'subtitle', value: 'test' },
|
||||
{ field: 'notes', value: 'test' },
|
||||
{ field: 'colour', value: 'test' },
|
||||
{ field: 'user0', value: 'test' },
|
||||
{ field: 'user1', value: 'test' },
|
||||
{ field: 'user2', value: 'test' },
|
||||
{ field: 'user3', value: 'test' },
|
||||
{ field: 'user4', value: 'test' },
|
||||
{ field: 'user5', value: 'test' },
|
||||
{ field: 'user6', value: 'test' },
|
||||
{ field: 'user7', value: 'test' },
|
||||
{ field: 'user8', value: 'test' },
|
||||
{ field: 'user9', value: 'test' },
|
||||
];
|
||||
|
||||
testFields.forEach((testCase) => {
|
||||
test(`${testCase.field}:${testCase.value}`, () => {
|
||||
expect(parseField(testCase.field, testCase.value)).toBe(testCase.value);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeTable()', () => {
|
||||
it('returns array of arrays with given fields', () => {
|
||||
const headerData = {};
|
||||
const tableData = [
|
||||
{
|
||||
title: 'test title 1',
|
||||
presenter: '',
|
||||
timeStart: 0,
|
||||
timeEnd: 0,
|
||||
isPublic: 'x',
|
||||
user0: 'test',
|
||||
user1: 'test',
|
||||
},
|
||||
];
|
||||
const userFields = {
|
||||
user0: 'test',
|
||||
};
|
||||
|
||||
const table = makeTable(headerData, tableData, userFields);
|
||||
expect(table).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('make CSV()', () => {
|
||||
it('joins an array of arrays with commas and newlines', () => {
|
||||
const testdata = [['field'], ['after newline', 'after comma'], ['', 'after empty']];
|
||||
expect(makeCSV(testdata)).toMatchInlineSnapshot(`
|
||||
"data:text/csv;charset=utf-8,field
|
||||
after newline,after comma
|
||||
,after empty
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
|
||||
|
||||
import { stringFromMillis } from '../../common/utils/time.js';
|
||||
|
||||
import EditableCell from './tableElements/EditableCell';
|
||||
|
||||
import style from './Table.module.scss';
|
||||
|
||||
/**
|
||||
* React - Table column object
|
||||
* @param sizes
|
||||
* @param userFields
|
||||
*/
|
||||
export const makeColumns = (sizes, userFields) => {
|
||||
return [
|
||||
{
|
||||
Header: 'Public',
|
||||
accessor: 'isPublic',
|
||||
Cell: ({ cell: { value } }) => (value ? <FiCheck className={style.check} /> : ''),
|
||||
width: sizes?.isPublic || 50,
|
||||
},
|
||||
{
|
||||
Header: 'Start',
|
||||
accessor: 'timeStart',
|
||||
Cell: ({ cell: { value, delayed } }) => stringFromMillis(delayed || value),
|
||||
width: sizes?.timeStart || 90,
|
||||
},
|
||||
{
|
||||
Header: 'End',
|
||||
accessor: 'timeEnd',
|
||||
Cell: ({ cell: { value, delayed } }) => stringFromMillis(delayed || value),
|
||||
width: sizes?.timeEnd || 90,
|
||||
},
|
||||
{
|
||||
Header: 'Duration',
|
||||
accessor: 'duration',
|
||||
Cell: ({ cell: { value } }) => stringFromMillis(value),
|
||||
width: sizes?.duration || 90,
|
||||
},
|
||||
{ Header: 'Title', accessor: 'title', width: sizes?.title || 400 },
|
||||
{ Header: 'Subtitle', accessor: 'subtitle', width: sizes?.subtitle || 350 },
|
||||
{ Header: 'Presenter', accessor: 'presenter', width: sizes?.presenter || 250 },
|
||||
{ Header: 'Notes', accessor: 'note', width: sizes?.note || 500 },
|
||||
{
|
||||
Header: userFields.user0 || 'User 0',
|
||||
accessor: 'user0',
|
||||
Cell: EditableCell,
|
||||
width: sizes?.user0 || 200,
|
||||
},
|
||||
{
|
||||
Header: userFields.user1 || 'User 1',
|
||||
accessor: 'user1',
|
||||
Cell: EditableCell,
|
||||
width: sizes?.user1 || 200,
|
||||
},
|
||||
{
|
||||
Header: userFields.user2 || 'User 2',
|
||||
accessor: 'user2',
|
||||
Cell: EditableCell,
|
||||
width: sizes?.user2 || 200,
|
||||
},
|
||||
{
|
||||
Header: userFields.user3 || 'User 3',
|
||||
accessor: 'user3',
|
||||
Cell: EditableCell,
|
||||
width: sizes?.user3 || 200,
|
||||
},
|
||||
{
|
||||
Header: userFields.user4 || 'User 4',
|
||||
accessor: 'user4',
|
||||
Cell: EditableCell,
|
||||
width: sizes?.user4 || 200,
|
||||
},
|
||||
{
|
||||
Header: userFields.user5 || 'User 5',
|
||||
accessor: 'user5',
|
||||
Cell: EditableCell,
|
||||
width: sizes?.user5 || 200,
|
||||
},
|
||||
{
|
||||
Header: userFields.user6 || 'User 6',
|
||||
accessor: 'user6',
|
||||
Cell: EditableCell,
|
||||
width: sizes?.user6 || 200,
|
||||
},
|
||||
{
|
||||
Header: userFields.user7 || 'User 7',
|
||||
accessor: 'user7',
|
||||
Cell: EditableCell,
|
||||
width: sizes?.user7 || 200,
|
||||
},
|
||||
{
|
||||
Header: userFields.user8 || 'User 8',
|
||||
accessor: 'user8',
|
||||
Cell: EditableCell,
|
||||
width: sizes?.user8 || 200,
|
||||
},
|
||||
{
|
||||
Header: userFields.user9 || 'User 9',
|
||||
accessor: 'user9',
|
||||
Cell: EditableCell,
|
||||
width: sizes?.user9 || 200,
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @description set default column order
|
||||
*/
|
||||
export const defaultColumnOrder = [
|
||||
'isPublic',
|
||||
'timeStart',
|
||||
'timeEnd',
|
||||
'duration',
|
||||
'title',
|
||||
'subtitle',
|
||||
'presenter',
|
||||
'note',
|
||||
'user0',
|
||||
'user1',
|
||||
'user2',
|
||||
'user3',
|
||||
'user4',
|
||||
'user5',
|
||||
'user6',
|
||||
'user7',
|
||||
'user8',
|
||||
'user9',
|
||||
];
|
||||
|
||||
/**
|
||||
* @description set default hidden columns
|
||||
*/
|
||||
export const defaultHiddenColumns = [
|
||||
'user0',
|
||||
'user1',
|
||||
'user2',
|
||||
'user3',
|
||||
'user4',
|
||||
'user5',
|
||||
'user6',
|
||||
'user7',
|
||||
'user8',
|
||||
'user9',
|
||||
];
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { AutoTextArea } from '@/common/components/input/auto-text-area/AutoTextArea';
|
||||
import { TableSettingsContext } from '@/common/context/TableSettingsContext';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* Shamelessly copied from react-table docs
|
||||
* Plugged into chakra-ui editable component
|
||||
* @description Custom editable field for table component
|
||||
* @param props
|
||||
* @return {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
export default function EditableCell(props) {
|
||||
const {
|
||||
value: initialValue,
|
||||
row: { index },
|
||||
column: { id },
|
||||
handleUpdate,
|
||||
} = props;
|
||||
const { theme } = useContext(TableSettingsContext);
|
||||
|
||||
// We need to keep and update the state of the cell normally
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
const onChange = useCallback((e) => setValue(e.target.value), []);
|
||||
|
||||
// We'll only update the external data when the input is blurred
|
||||
const onBlur = useCallback(() => handleUpdate(index, id, value), [handleUpdate, id, index, value]);
|
||||
|
||||
|
||||
// If the initialValue is changed external, sync it up with our state
|
||||
useEffect(() => {
|
||||
setValue(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
return (
|
||||
<AutoTextArea
|
||||
size='sm'
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
rows={3}
|
||||
transition='none'
|
||||
spellCheck={false}
|
||||
isDark={theme === "dark"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
EditableCell.propTypes = {
|
||||
value: PropTypes.string,
|
||||
row: PropTypes.object,
|
||||
column: PropTypes.object,
|
||||
handleUpdate: PropTypes.func,
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import { IoPause } from '@react-icons/all-files/io5/IoPause';
|
||||
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
||||
import { IoStop } from '@react-icons/all-files/io5/IoStop';
|
||||
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { tooltipDelayFast } from '../../../ontimeConfig';
|
||||
|
||||
export default function PlaybackIcon(props) {
|
||||
const { state } = props;
|
||||
|
||||
if (state === 'stop') {
|
||||
return (
|
||||
<Tooltip openDelay={tooltipDelayFast} label='Timer Stopped' shouldWrapChildren>
|
||||
<IoStop />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (state === 'start') {
|
||||
return (
|
||||
<Tooltip openDelay={tooltipDelayFast} label='Timer Playing' shouldWrapChildren>
|
||||
<IoPlay />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (state === 'pause') {
|
||||
return (
|
||||
<Tooltip openDelay={tooltipDelayFast} label='Timer Paused' shouldWrapChildren>
|
||||
<IoPause />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (state === 'roll') {
|
||||
return (
|
||||
<Tooltip openDelay={tooltipDelayFast} label='Timer Rolling' shouldWrapChildren>
|
||||
<IoTimeOutline />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
PlaybackIcon.propTypes = {
|
||||
state: PropTypes.string,
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { tooltipDelayFast } from '../../../ontimeConfig';
|
||||
|
||||
import styles from '../Table.module.scss';
|
||||
|
||||
export default function SortableCell({ column }) {
|
||||
const { style, ...restColumn } = column.getHeaderProps();
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: column.id,
|
||||
});
|
||||
|
||||
// prevent scaling on drag
|
||||
const cssTransform = {
|
||||
...transform,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
}
|
||||
|
||||
// build drag styles
|
||||
const dragStyle = {
|
||||
transform: CSS.Transform.toString(cssTransform),
|
||||
transition,
|
||||
...style,
|
||||
};
|
||||
|
||||
return (
|
||||
<th {...restColumn} ref={setNodeRef} style={{...dragStyle}} className={isDragging ? styles.dragging: ''}>
|
||||
<div {...attributes} {...listeners}>
|
||||
<Tooltip label={column.Header} openDelay={tooltipDelayFast}>
|
||||
{column.render('Header')}
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div {...column.getResizerProps()} className={styles.resizer} />
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
||||
SortableCell.propTypes = {
|
||||
column: PropTypes.object.isRequired,
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import style from '../Table.module.scss';
|
||||
|
||||
// reusable button styles
|
||||
const buttonProps = {
|
||||
colorScheme: 'blue',
|
||||
size: 'sm',
|
||||
variant: 'ghost',
|
||||
};
|
||||
|
||||
export default function TableSettings(props) {
|
||||
const {
|
||||
columns,
|
||||
handleResetResizing,
|
||||
handleResetReordering,
|
||||
handleResetToggles,
|
||||
handleClearToggles,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={style.tableSettings}>
|
||||
<div className={style.hSeparator}>Select and order fields to show in table</div>
|
||||
<div className={style.options}>
|
||||
{columns.map((column) => (
|
||||
<label key={column.id}>
|
||||
<input type='checkbox' {...column.getToggleHiddenProps()} /> {column.Header}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div className={style.buttonRow}>
|
||||
<Button onClick={handleResetResizing} {...buttonProps}>
|
||||
Reset Resizing
|
||||
</Button>
|
||||
<Button onClick={handleResetReordering} {...buttonProps}>
|
||||
Reset Reordering
|
||||
</Button>
|
||||
<Button onClick={handleResetToggles} {...buttonProps}>
|
||||
Reset Toggles
|
||||
</Button>
|
||||
<Button onClick={handleClearToggles} {...buttonProps}>
|
||||
Show All
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TableSettings.propTypes = {
|
||||
columns: PropTypes.array,
|
||||
handleResetResizing: PropTypes.func.isRequired,
|
||||
handleResetReordering: PropTypes.func.isRequired,
|
||||
handleResetToggles: PropTypes.func.isRequired,
|
||||
handleClearToggles: PropTypes.func.isRequired,
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import style from '../Table.module.scss';
|
||||
|
||||
export default function BlockRow(props) {
|
||||
const { row } = props;
|
||||
return (
|
||||
<tr {...row.getRowProps()}>
|
||||
<td className={style.blockCell}>Delay Block</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
BlockRow.propTypes = {
|
||||
row: PropTypes.object.isRequired,
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { millisToMinutes } from '../../../common/utils/dateConfig';
|
||||
|
||||
import style from '../Table.module.scss';
|
||||
|
||||
export default function DelayRow(props) {
|
||||
const { row } = props;
|
||||
const delayVal = row.original.duration;
|
||||
const minutesDelayed = Math.abs(millisToMinutes(delayVal));
|
||||
const labelText = `${minutesDelayed} minutes ${delayVal >= 0 ? 'delayed' : 'ahead'}`;
|
||||
|
||||
return (
|
||||
<tr {...row.getRowProps()}>
|
||||
<td className={style.delayCell}>{labelText}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
DelayRow.propTypes = {
|
||||
row: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||
|
||||
import style from '../Table.module.scss';
|
||||
|
||||
export default function EventRow(props) {
|
||||
const { row, index, selectedId, delay } = props;
|
||||
const selected = row.original.id === selectedId;
|
||||
|
||||
const colours = row.original.colour
|
||||
? getAccessibleColour(row.original.colour)
|
||||
: {};
|
||||
|
||||
return (
|
||||
<tr {...row.getRowProps()} className={selected ? style.selected : ''} id={row.original.id}>
|
||||
<td className={style.indexColumn}>{index}</td>
|
||||
{row.cells.map((cell) => {
|
||||
const { key, style, ...restCellProps } = cell.getCellProps();
|
||||
const dynamicStyles = { ...style, ...colours };
|
||||
|
||||
|
||||
// Inject delay value if exits
|
||||
if (delay !== 0 && delay != null) {
|
||||
const col = cell.column.Header;
|
||||
if (col === 'End' || col === 'Start') {
|
||||
cell.delayed = cell.value + delay;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<td key={key} style={{ ...dynamicStyles }} {...restCellProps}>
|
||||
{cell.render('Cell')}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
EventRow.propTypes = {
|
||||
row: PropTypes.object.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
selectedId: PropTypes.string,
|
||||
delay: PropTypes.number,
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
import { stringify } from 'csv-stringify/browser/esm/sync';
|
||||
|
||||
/**
|
||||
* @description parses a field for export
|
||||
* @param {string} field
|
||||
* @param {*} data
|
||||
* @return {string}
|
||||
*/
|
||||
import { stringFromMillis } from '../../common/utils/time';
|
||||
|
||||
export const parseField = (field, data) => {
|
||||
let val;
|
||||
switch (field) {
|
||||
case 'timeStart':
|
||||
case 'timeEnd':
|
||||
val = stringFromMillis(data);
|
||||
break;
|
||||
case 'isPublic':
|
||||
val = data ? 'x' : '';
|
||||
break;
|
||||
default:
|
||||
val = data;
|
||||
break;
|
||||
}
|
||||
if (typeof data === 'undefined') {
|
||||
return ''
|
||||
}
|
||||
return val;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Creates an array of arrays usable by xlsx for export
|
||||
* @param {object} headerData
|
||||
* @param {array} tableData
|
||||
* @param {object} userFields
|
||||
* @return {(string[])[]}
|
||||
*/
|
||||
export const makeTable = (headerData, tableData, userFields) => {
|
||||
const data = [
|
||||
['Ontime · Schedule Template'],
|
||||
['Event Name', headerData?.title || ''],
|
||||
['Event URL', headerData?.url || ''],
|
||||
[],
|
||||
];
|
||||
|
||||
const fieldOrder = [
|
||||
'timeStart',
|
||||
'timeEnd',
|
||||
'title',
|
||||
'presenter',
|
||||
'subtitle',
|
||||
'isPublic',
|
||||
'notes',
|
||||
'colour',
|
||||
'user0',
|
||||
'user1',
|
||||
'user2',
|
||||
'user3',
|
||||
'user4',
|
||||
'user5',
|
||||
'user6',
|
||||
'user7',
|
||||
'user8',
|
||||
'user9',
|
||||
];
|
||||
|
||||
const fieldTitles = [
|
||||
'Time Start',
|
||||
'Time End',
|
||||
'Event Title',
|
||||
'Presenter Name',
|
||||
'Event Subtitle',
|
||||
'Is Public? (x)',
|
||||
'Notes',
|
||||
'Colour',
|
||||
];
|
||||
|
||||
for (const field in userFields) {
|
||||
const fieldValue = userFields[field];
|
||||
const displayName = `${field}${
|
||||
fieldValue !== field && fieldValue !== '' ? `:${fieldValue}` : ''
|
||||
}`;
|
||||
fieldTitles.push(displayName);
|
||||
}
|
||||
|
||||
data.push(fieldTitles);
|
||||
|
||||
tableData.forEach((entry) => {
|
||||
const row = [];
|
||||
fieldOrder.forEach((field) => row.push(parseField(field, entry[field])));
|
||||
data.push(row);
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Converts an array of arrays to a csv file
|
||||
* @param {array[]} arrayOfArrays
|
||||
* @return {string}
|
||||
*/
|
||||
export const makeCSV = (arrayOfArrays) => {
|
||||
let csvData = 'data:text/csv;charset=utf-8,';
|
||||
const stringifiedData = stringify(arrayOfArrays);
|
||||
return csvData + stringifiedData;
|
||||
};
|
||||
Reference in New Issue
Block a user