* Refactor/ts sockets (#210)

* refactor(ts): create socket subscription service
* refactor(ts-sockets): useSubscription hook
* refactor(ts-sockets): extract colour selection to utility

* Refactor/socket controller (#212)

* refactor(socket): dictionary cleanup
* refactor(socket): detangle socket from EventTimer
* refactor(socket): poll object
* refactor(socket): wip, extract data responsibilities to provider
* fix: issue with onAir
* refactor: create data provider and validation utils
* refactor: remove deprecated endpoint
* refactor: detangle and validate ontime controller
* refactor: detangle and validate events controller
* refactor: validate routers
* refactor: handle post failure in modals

* chore: upgrade electron (#215)

* Fix: issue with text colour (#214)
* fix: adjust text colour from context
* fix: issue with wrong proptype
* chore: upgrade electron
* chore: declare node version
* chore: upgrade node actions

* Feat/55 v2 (#225)

* chore: upgrade relevant libraries
* feat(skip): add skip styling to paginated items
* chore: upgrade relevant libraries
* Fix: issue with text colour (#214)
* fix: adjust text colour from context
* fix: issue with wrong proptype
* fix issue with vite migration (#217)
* hotfix: 1.8.2 issues vite migration
* fix: file import options
* feat(55): styling
* refactor: remove onhover option for entry block
* refactor: relocate logging provider
* refactor: convert to typescript
* feat(55): add event editor
* refactor: extract event actions
* fix: bad import
* feat(55): redesign components
* fix: issues with not awaiting async
* refactor: replace socket with subscription
* refactor: remove unused
* style: small tweaks
* refactor: extract event actions
* refactor: cleanup debug
* refactor: chakra imports
* fix: optimistic mutations
* refactor: handle promise rejections
* refactor: validation
* fix: rq optimistic mutations
* feat: add playback feedback to block
* refactor: no optimistic adding of events
* refactor: simplify cursor state
* styles: cleanup editor style
* refactor: cleanup duration update
* fix: revert package upgrade (issues with vitest)
* fix: prevent cyclic imports
* refactor: extract data fetcher
* refactor: typescript migration
* chore: update tests

* fix: remove problematic (#228)

* styles: v2 (#229)

* style: revise selected style
* style: revise editor styles
* style: revise viewer styles
* chore: update tests

* v2:feat (#231)

* style: element relationship
* feat: utility to add first element
* refactor: organise dir
* ux: deleting element closes drawer
* fix: issue with escaped characters
* fix: issue with overflow in events list
* fix: prevent event timer from receiving non event
* style: prevent small shift on class change
* style: visually detach event editor
* fix: entry block knows last event
* fix: skip event timer updates on non events
* fix: prevent electron OPENGL error
* style: cleanup text styles
* style: unify input styles
* refactor: simplify style and composition
* refactor: tweaks on style + ts
* refactor: add validation to events post
* fix: handle non events in event timer
* refactor: cleanup unused

* V2 styles part2 (#233)

* style: create tag to display network messages
* style: create button style for playback
* style: review style for QuickEntry
* style: review style for Info panel
* style: review style for Event blocks
* style: review style for Message control panel
* style: review style for Playback control
* style: review application styles
* style: review block styles
* refactor: TapButton has active state
* style: small style tweaks
* refactor: type checks

* V2 message manager (#234)

* refactor: server run script
* refactor: extract message control feature
* refactor: socket usage for message control feature
* refactor: migrate e2e testing to playwright
* chore: add feature tests
* refactor: add validation on socket controller
* chore: jest tests server logic
* chore: update tests
* chore: run playwright tests on pull_requests

* V2 rundown manager (#237)

* refactor: migrate eventlist > rundown

* hotfix: fix object naming error on excel import (#252)

* feat: V2 ontime menu (#259)

* V2 socket store (#254)

* refactor: create socket utility
* refactor: ensure a single subscription to topics
* refactor: replace usages of socket object with subscription store handler

* refactor: v2 event loader (#260)

* refactor: extract jest config

* refactor: rename params in reoder endpoint

* refactor: avoid app exports

* refactor: small code smells

* refactor: run development server

* refactor: extract rundown service and event loader logic

* refactor: rename events > rundown

* refactor: code style

* fix: handle external change of title

* refactor: cleanup dictionary

* refactor: rename MessageService

* refactor: use eventID for operations

* refactor: migrate OSC controller to service

* refactor: migrate Socket controller to service

* refactor: migrate HTTP controller to service

* refactor: extract logic into discrete services

* refactor: remove rundown from event timer

* refactor: remove duplicate

* refactor: remove unused

* chore: update tests

* refactor: run test CI on master or manual run

* lint: reorder imports

* refactor: ensure consistent routing

* V2 event block (#261)

* feat: implement progress bar on running event

* refactor: use event action hook

* refactor: align backend data

* v2 styling (#263)

* feat: implement progress bar on running event

* refactor: use event action hook

* refactor: align backend data

* style: v2 style tweaks

* style: v2 style tweaks + ts <Info>

* fix: fix delay increment calls

* style: v2 style tweaks + ts <Playback>

* style: v2 style tweaks + ts <EventEditor>

* style: v2 style tweaks + ts <MessageControl>

* fix: naming issue with message control socket endpoint

* chore: upgrade style dependencies

* style: v2 style tweaks + ts <MenuBar>

* style: v2 style tweaks + ts <RundownMenu>

* style: v2 style tweaks + ts <Playback>

* fix: add selected and next information to EventTimer

* style: v2 style tweaks + ts <EventBlock>

* style: v2 style tweaks + ts <DelayBlock>

* style: v2 style tweaks + ts <BlockBlock>

* style: v2 style tweaks + ts <QuickEntry>

* refactor: extract <TextInput> logic

* chore: upgrade build dependencies

* chore: folder structure refactor

* fix: issue with dependency path in electron

* chore: update version in demo db

* v2: refactor (#264)

* chore: upgrade dependencies

* chore: remove unused

* fix: issue with missing index data

* fix: discrepancy on counting events

* fix: swapped classes

* fix: prevent attempting load in empty list

* chore: cleanup completed

* chore: remove unused

* refactor: convert to typescript

* refactor: migrate to new endpoints

* refactor: convert to typescript

* fix: prevent issue with undefined process

* fix: small code smells

* fix: issue with missing version variable on build

* refactor: configure retries on data fetching

* style: design review

* fix: prevent cursor out of range

* lint: react query linting

* style: checkbox styles

* v2 event editor (#265)

* style: add cursor indicator in all blocks

* style: prevent reflows on width

* style: harmonise white range

* ux: focus elements on cursor nav

* ux: prevent focus on indicators

* refactor: convert to typescript

* fix: missing dependency

* chore: add linter rules for dependency arrays

* style: style editable component

* style: tweak input styles

* style: tweak switch styles

* style: style tweaks

* V2 sentry (#267)

* chore: add sentry to frontend

* chore: add sentry to server

* chore: report only in production

* chore: add sentry source maps

* v2 alpha 3 (#272)

* style: correct background colours

* chore: add folder resolutions to vite

* refactor: add auto refetch to HTTP APIs

* fix: view settings override endpoint

* refactor: memoize callbacks

* fix: reorder reaching wrong data adapter

* refactor: memoize callbacks

* style: presentation cleanup

* fix(delete): prevent flow with deleted event in playback

* refactor: convert to typescript

* refactor: small fixes and typescript conversion

* ux: improve feedback on local changes

* refactor: cleanup props

* v2 event timer (#274)

* lint: cleanup unused

* refactor: rename component to avoid conflict

* style: gap in element row

* refactor: rename playstate -> playback

* refactor: cleanup usages of timer and prepare integration manager

* style: re-arrange button order

* refactor: improve event loading

* feat(timer-service): hot reload

* fix: issue with duration input

* chore: cleanup debug

* refactor: resolve poll from runtime store

* refactor: cleanup merge

* refactor: small improvements in timer hot-reload

* v2: views (#278)

* config(sentry): instrumentation only in production

* chore: upgrade deps

* refactor: simplify and convert to ts

* refactor: composition and styles

* chore: remove deprecated deps

* ux: close menu on click outside

* style: small tweaks and clarifications

* refactor: retire PiP view

* ux: small tweaks

* style: cleanup deprecated styles

* chore: remove unused

* refactor: remove usages of deprecated fields and improve null timer presentation

* refactor: improve composition of countdown

* style: cleanup overridable styles

* ux: show event overtime status

* style: correct logic in paused timers

* v2: roll (#280)

* ux: delete flow

* style: small tweaks in interface

* refactor: gracefully quit on error

* fix: logic around updating events

* chore: cleanup dictionary

* refactor: improve DX on creating aux files

* fix: safe destructure function return

* refactor: safe handling of falsy timer values

* fix: improve ux on stopping roll mode

* chore: upgrade deps

* feat: roll mode

* refactor: remove unused

* 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>

* Fix: #279 End Message in Minimal Timer (#286)

* fix: Minimal View #279 end message
---------

Co-authored-by: Fabian Posenau <fabian@fphome.de>

* hotfix: resolve data directory (#288)

* Feature/#276/qrcodes (#289)

* bugfix: wrong format when on 12h format

* update: website link in placeholder

---------

Co-authored-by: Fabian Posenau <fabian@fphome.de>
Co-authored-by: cv <34649812+cpvalente@users.noreply.github.com>

* V2 types (#291)

* V2 integration (#293)

* feat: integrations and lifecycles

* refactor: prevent issues with start order

* V2 fix offline (#294)

* fix: resolve path to UI build in dev

* fix: configure offline fetching

* V2 fix socket (#296)

* refactor: rename timer endpoint

* refactor: typescript

* refactor: rename eventData and viewSettings

* refactor: message manager uses event store

* style: alpha4 style tweaks (#299)

* fix: backgroundcolor for .blockNotes (#300)

Co-authored-by: Fabian Posenau <fabian@fphome.de>

* Feat/countdown style (#290)

* feat: Countdown Style

---------

Co-authored-by: Fabian Posenau <fabian@fphome.de>
Co-authored-by: cv <34649812+cpvalente@users.noreply.github.com>
Co-authored-by: Fabian Posenau <19673098+kellhogs@users.noreply.github.com>

* fix: wrong parameter (#302)

Co-authored-by: Fabian Posenau <fabian@fphome.de>

* V2 fix timer (#304)

* Feat/update check (#303)

* feat: show latest version in settings modal

---------

Co-authored-by: Fabian Posenau <fabian@fphome.de>

* improve time input (#307)

* add the ability to parse time like 2h20m

---------

Co-authored-by: Fabian Posenau <fabian@fphome.de>

* V2 ws store (#310)

* Update TimerService.ts

* refactor: message service publishes to store

* refactor: several type improvements

* V2 ws store wss (#309)

* refactor: shared logging types

* refactor: simplify message service consumption

* refactor: create discrete logging system

* refactor: move socket.io > websocket

* Feat/end action (#308)

* feat: end action

---------

Co-authored-by: Fabian Posenau <fabian@fphome.de>
Co-authored-by: cv <34649812+cpvalente@users.noreply.github.com>
Co-authored-by: Fabian Posenau <19673098+kellhogs@users.noreply.github.com>

* V2 osc (#313)

* chore: upgrade relevant deps

* fix: electron app to tray

* chore: cleanup dictionary

* feat: handle several messages in a event

* ux: disable irrelevant buttons in browser

* feat: parse and validate subscriptions

* feat: create UI for OSC Integration

* fix: cleanup logger behaviour

* feat: allow OSC settings to be changed at runtime

* v2 alpha5 (#315)

* fix: fetch in offline environments (#295)

* add arm platforms to docker build

---------

Co-authored-by: Fabian Posenau <fabian@fphome.de>

* fix: docker build (#298)

Co-authored-by: Fabian Posenau <fabian@fphome.de>

* Timer: fix too many renders error when using ?progress (#305)

* ux: remove duplicate showing of selected event

* several small fixes and UX improvements
---------

Co-authored-by: Fabian Posenau <fabian.p99@gmx.de>
Co-authored-by: Fabian Posenau <fabian@fphome.de>
Co-authored-by: Marks Polakovs <github@markspolakovs.me>

* V2 docker (#312)

* Create docker-ci.yml

* docker build

* add build app

* path changes

* delete double workflow

* update docker run template

* cleanup

* resolve wierd path issue

* wrong path

* again wrong path

* rename docker releases

* move styles population and change to function call

* re-add old build pipeline

* rename buld file

* feractor init function

* refactor: startDb to initAssets

* refactor: startDB to initAssets

* fix: docker.cjs building whole app

* fix: docker environment

* update docker paths

* update: build command

* cleanup

* cleanup: commented code

---------

Co-authored-by: Fabian Posenau <fabian@fphome.de>

* v2 lazy rundown (#317)

* chore: update related deps

* refactor: memoise entrypoints

* refactor: store selectors

* refactor: improve component performance

* chore: remove unused dependencies

* refactor: improve component dnd

* V2 build (#319)

* chore: build pipeline for v2

* chore: run unit tests

* chore: configure sentry build

* Update test_v2.yml

* Update test_v2.yml

* V2 e2e (#320)

* chore: cleanup handling of a bad read

* chore: configure e2e testing

* chore: version bump

* V2 align (#321)

* fix: fetch in offline environments (#295)

* Add arm platforms to docker build (#297)

* add arm platforms to docker build

---------

Co-authored-by: Fabian Posenau <fabian@fphome.de>

* fix: docker build (#298)

Co-authored-by: Fabian Posenau <fabian@fphome.de>

* Timer: fix too many renders error when using ?progress (#305)

* Timer: fix too many renders error when using ?progress

* version bump (#314)

* style: small tweaks

* fix: prevent actions in roll mode

* chore: update external assets

* style: show shortcuts in quick blocks

* refactor: revert optimisations on callbacks

* fun: industry dictionary

* style: indicators for event features

---------

Co-authored-by: Fabian Posenau <fabian.p99@gmx.de>
Co-authored-by: Fabian Posenau <fabian@fphome.de>
Co-authored-by: Marks Polakovs <github@markspolakovs.me>

* refactor: ensure start order (#322)

* Update build_docker_v2.yml

* V2 fix roll (#323)

* chore: profile node

* fix: always roll

* Add translations for views (#325) (#328)

* Add translations for views (#325)

---------

Co-authored-by: Kevin <60135953+kev-ac@users.noreply.github.com>

* feat: notify ws clients on rundown changes (#327)

* V2 beta1 (#326)

* style: labels on added time

* style: remove mentions of PiP

* refactor: unify usage of ms for timers

* refactor: create events with 0 duration

* style: several small tweaks

* refactor: keep block when applying delays

* feat: blocks have titles

* style: improvements in time entry warnings

* style: override progress bar styles

* style: prevent overflow

* feat: show character count in editor

* refactor: provide initial payload

* refactor: lower test boundary

* refactor: get colour from swatches

* style: rename title block

* chore: version bump

* style: fix info scroll

* refactor: performance tweaks to event editor

* chore: upgrade deps

* refactor: prevent iterating on undefined

* style: small style tweaks

* fix: close editor on event delete

* refactor: improve performance on EventBlock

* style: small tweaks on blocks

* chore: upgrade deps

* chore: upgrade deps

* style: ensure events are always visible

* Fix/events enpoint (#329)

* fix: sentry hangs node process

* add onClick handler (#335)

Co-authored-by: Fabian Posenau <fabian@fphome.de>

* fix docker on different port (#337)

Co-authored-by: Fabian Posenau <fabian@fphome.de>

* V2 beta3 (#338)

* chore: upgrade local build documentation

* style: keep scrolling event in screen

* feat: delay is time entry

* refactor: remove unused

* refactor: remove unused

* refactor: batch store updates

* refactor: virtually remove cap on events

* refactor: style and behaviour tweaks to event block

* style: tweaks on schedules

* chore: remove sentry from server

* style: reorder menu

* chore: update docs

* fix: resolve to static port in dev mode

* V2 fix delay (#340)

* fix: avoid modifying original objects

* refactor: serve react as compressed br (#339)

* refactor: changes in time input

* V2 e2e playwright (#343)

* test: create utility for uploading test-db

* chore: upgrade deps

* refactor: remove unused functions

* chore: create testing boilerplate

* test: upload showfile

* refactor: parse time input (#351)

* chore: vite upgrade (#354)

* chore: upgrade vite

* feat: edit mode (#344)

* style: add mode selection to menu

* style: rename rundown functions

* feat: edit mode

* fix: osc modal (#352)

* refactor: parse time input

* fix: form dirty flag on dynamic elements

* style: cuesheet block (#355)

* style: cuesheet show block title

* style: cuesheet show full delay string

* v2 about (#362)

* chore: remove unused assets

* feat: about ontime

* refactor: cleanup unused

* v2 beta 4 (#361)

* ux: rename end action

* chore: update demo db

* style: tweaks on extracted rundown

* chore: prevent console logs in production code

* feat: go mode

* style: remove window size limits

* style: rename delete action

* style: small tweaks on icons

* fix: style override on params

* chore: update test db

* refactor: small code quality improvements

* refactor: prevent circular imports

* fix v2 roll timer (#363)

* fix: roll update skips elapsed time

* refactor: remove unused data

* style: fix style in roll state

* ux: content aware playback (#364)

* v2 beta 4 bump (#367)

* chore: update links to documentation

* chore: version bump

* Feature: quick start (#369)

* feat: quick start modal

* refactor: end message is part of view settings

* refactor: small tweaks and type improvements

* Feat/timer/stylechange (#341)

* feat: multi color progress bar

* add color support to minimal timer

Co-Authored-By: Fabian Posenau <19673098+kellhogs@users.noreply.github.com>

---------

Co-authored-by: Fabian Posenau <fabian@fphome.de>
Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>
Co-authored-by: Fabian Posenau <19673098+kellhogs@users.noreply.github.com>

* v2 beta5 modals (#377)

* refactor: folder structure

* style: modal wrapper

* feat: add nordic translations

* v2 beta 5 (#375)

* chore: update documentation links

* fix: sentry has no access to error context

* style: clarify event history

* fix: issue with clipboard write in safari

* style: clarify event history

* refactor: improvements to follow logic in rundown

* refactor: improvements in go mode

* several style tweaks and small improvements

* Ux language (#378)

* ux: language from menu

* feat: add extra support for languages

german
portuguese
spanish

* refactor: type improvements

* v2 beta6 (#379)

* feat: operator key

* chore: update demo

* chore: version bump

* fix: prevent stale keys

* refactor: add new fields to validation

* style: tweaks to modal arrangement

* refactor: prevent parsing http

* ux: click anywhere in event to edit

* refactor: performance improvements to time input

* refactor: performance improvements menu

* V2 beta 6 (#381)

* fix: fetch in offline environments (#295)

* Add arm platforms to docker build (#297)

* add arm platforms to docker build

---------

Co-authored-by: Fabian Posenau <fabian@fphome.de>

* fix: docker build (#298)

Co-authored-by: Fabian Posenau <fabian@fphome.de>

* Timer: fix too many renders error when using ?progress (#305)

* Timer: fix too many renders error when using ?progress

* version bump (#314)

* chore: remove unused assets

* refactor: improve rundown loading time

* style: rename project file

* chore: update demo files

* refactor: improve loading time

* chore: upgrade upload packages

* refactor: modal redesign and ux improvements

* refactor: type and ux improvements on translations

* refactor: prevent flash on suspense

* refactor: remove unnecessary import

* chore: update tests

* refactor: cleanup imports

* fix: prevent flash on suspense

---------

Co-authored-by: Fabian Posenau <fabian.p99@gmx.de>
Co-authored-by: Fabian Posenau <fabian@fphome.de>
Co-authored-by: Marks Polakovs <github@markspolakovs.me>

* style views (#390)

* style: animate transition from left side

* refactor: improve schedule relationship

* refactor: prevent passing unhandled value

* style: remove inner radius in borders

---------

Co-authored-by: Fabian Posenau <fabian.p99@gmx.de>
Co-authored-by: Fabian Posenau <fabian@fphome.de>
Co-authored-by: Fabian Posenau <19673098+kellhogs@users.noreply.github.com>
Co-authored-by: Marks Polakovs <github@markspolakovs.me>
Co-authored-by: Kevin <60135953+kev-ac@users.noreply.github.com>
This commit is contained in:
Carlos Valente
2023-05-18 14:02:22 +02:00
committed by GitHub
parent 974a239b67
commit c8e979bc9a
698 changed files with 31752 additions and 34071 deletions
@@ -0,0 +1,245 @@
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 permission='operator'>
<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,106 @@
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 useEventData from '../../common/hooks-query/useEventData';
import { formatDisplay } 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 timer = useTimer();
const { isFullScreen, toggleFullScreen } = useFullscreen();
const { data: event } = useEventData();
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(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,97 @@
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 './tableUtils';
import style from './Table.module.scss';
export default function TableWrapper() {
const { data: rundown } = useRundown();
const { data: userFields } = useUserFields();
const { updateEvent } = useEventAction();
const featureData = useCuesheet();
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,53 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`makeTable() > returns array of arrays with given fields 1`] = `
[
[
"Ontime · Schedule Template",
],
[
"Event Name",
"",
],
[
"Public URL",
"",
],
[
"Backstage 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",
"",
"",
"",
"",
"",
"",
"",
"",
],
]
`;
@@ -0,0 +1,93 @@
import { makeCSV, makeTable, parseField } from '../tableUtils';
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
"
`);
});
});
+104
View File
@@ -0,0 +1,104 @@
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
import EditableCell from './tableElements/EditableCell';
import style from './Table.module.scss';
import { millisToString } from 'ontime-utils';
/**
* 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 } }) => millisToString(delayed || value),
width: sizes?.timeStart || 90,
},
{
Header: 'End',
accessor: 'timeEnd',
Cell: ({ cell: { value, delayed } }) => millisToString(delayed || value),
width: sizes?.timeEnd || 90,
},
{
Header: 'Duration',
accessor: 'duration',
Cell: ({ cell: { value } }) => millisToString(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 { Playback } from 'ontime-types';
import { tooltipDelayFast } from '../../../ontimeConfig';
interface PlaybackIconProps {
state: Playback;
}
export default function PlaybackIcon(props: PlaybackIconProps) {
const { state } = props;
if (state === Playback.Stop) {
return (
<Tooltip openDelay={tooltipDelayFast} label='Timer Stopped' shouldWrapChildren>
<IoStop />
</Tooltip>
);
}
if (state === Playback.Play) {
return (
<Tooltip openDelay={tooltipDelayFast} label='Timer Playing' shouldWrapChildren>
<IoPlay />
</Tooltip>
);
}
if (state === Playback.Pause) {
return (
<Tooltip openDelay={tooltipDelayFast} label='Timer Paused' shouldWrapChildren>
<IoPause />
</Tooltip>
);
}
if (state === Playback.Roll) {
return (
<Tooltip openDelay={tooltipDelayFast} label='Timer Rolling' shouldWrapChildren>
<IoTimeOutline />
</Tooltip>
);
}
return '';
}
@@ -0,0 +1,37 @@
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,
});
// build drag styles
const dragStyle = {
...style,
transform: CSS.Translate.toString(transform),
transition,
};
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}>{row.original?.title || 'Block'}</td>
</tr>
);
}
BlockRow.propTypes = {
row: PropTypes.object.isRequired,
};
@@ -0,0 +1,21 @@
import PropTypes from 'prop-types';
import { millisToDelayString } from '../../../common/utils/dateConfig';
import style from '../Table.module.scss';
export default function DelayRow(props) {
const { row } = props;
const delayVal = row.original.duration;
const delayTime = delayVal !== 0 ? millisToDelayString(delayVal) : null;
return (
<tr {...row.getRowProps()}>
<td className={style.delayCell}>{delayTime}</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,105 @@
import { stringify } from 'csv-stringify/browser/esm/sync';
import { millisToString } from 'ontime-utils';
/**
* @description parses a field for export
* @param {string} field
* @param {*} data
* @return {string}
*/
export const parseField = (field, data) => {
let val;
switch (field) {
case 'timeStart':
case 'timeEnd':
val = millisToString(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 || ''],
['Public URL', headerData?.publicUrl || ''],
['Backstage URL', headerData?.backstageUrl || ''],
[],
];
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;
};