From c810d263f6e3c3579c068a47d0ee0051940dbfae Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 18 Dec 2024 13:28:56 +0000 Subject: [PATCH 1/3] chore: convert some class react components to functional --- .../lib/ConnectionStatusNotification.tsx | 38 +--- .../notifications/NotificationCenterPanel.tsx | 6 - .../src/client/ui/ClockView/Timediff.tsx | 31 ++- .../src/client/ui/PieceIcons/PieceIcon.tsx | 12 +- .../ui/PieceIcons/Renderers/CamInputIcon.tsx | 72 +++---- .../Renderers/GraphicsInputIcon.tsx | 55 +++-- .../Renderers/LiveSpeakInputIcon.tsx | 66 +++--- .../PieceIcons/Renderers/RemoteInputIcon.tsx | 12 +- .../PieceIcons/Renderers/UnknownInputIcon.tsx | 58 +++-- .../ui/PieceIcons/Renderers/VTInputIcon.tsx | 56 +++-- packages/webui/src/client/ui/RundownView.tsx | 132 ++++++------ .../ui/RundownView/PlaylistLoopingHeader.tsx | 12 +- .../ui/RundownView/RundownDividerHeader.tsx | 77 +++---- .../RundownTiming/PlaylistEndTiming.tsx | 198 ++++++++---------- .../RundownTiming/PlaylistStartTiming.tsx | 121 ++++++----- .../RundownView/RundownTiming/RundownName.tsx | 154 +++++++------- .../RundownView/RundownTiming/TimeOfDay.tsx | 18 +- .../ui/SegmentTimeline/BreakSegment.tsx | 54 ++--- .../src/client/ui/Shelf/EndWordsPanel.tsx | 89 ++++---- .../ItemRenderers/ItemRendererFactory.ts | 2 +- .../ItemRenderers/NoraItemRenderer.tsx | 94 ++++----- .../client/ui/Shelf/NextBreakTimingPanel.tsx | 42 ---- .../src/client/ui/Shelf/NextInfoPanel.tsx | 101 +++++---- .../src/client/ui/Shelf/PartNamePanel.tsx | 114 ++++------ .../src/client/ui/Shelf/PartTimingPanel.tsx | 107 +++++----- .../client/ui/Shelf/PlaylistEndTimerPanel.tsx | 1 - .../src/client/ui/Shelf/PlaylistNamePanel.tsx | 82 +++----- .../src/client/ui/Shelf/SegmentNamePanel.tsx | 62 ++---- .../client/ui/Shelf/SegmentTimingPanel.tsx | 77 ++++--- .../webui/src/client/ui/Status/DebugState.tsx | 109 +++++----- 30 files changed, 890 insertions(+), 1162 deletions(-) delete mode 100644 packages/webui/src/client/ui/Shelf/NextBreakTimingPanel.tsx diff --git a/packages/webui/src/client/lib/ConnectionStatusNotification.tsx b/packages/webui/src/client/lib/ConnectionStatusNotification.tsx index fcb7e66e50..f7995bb567 100644 --- a/packages/webui/src/client/lib/ConnectionStatusNotification.tsx +++ b/packages/webui/src/client/lib/ConnectionStatusNotification.tsx @@ -2,10 +2,7 @@ import { Meteor } from 'meteor/meteor' import { DDP } from 'meteor/ddp' import * as React from 'react' import * as _ from 'underscore' - -import { Translated } from './ReactMeteorData/react-meteor-data' import { MomentFromNow } from './Moment' - import { NotificationCenter, NoticeLevel, @@ -14,13 +11,14 @@ import { NotifierHandle, } from './notifications/notifications' import { WithManagedTracker } from './reactiveData/reactiveDataHelper' -import { withTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next' import { NotificationCenterPopUps } from './notifications/NotificationCenterPanel' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { ICoreSystem, ServiceMessage, Criticality } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' import { TFunction } from 'react-i18next' import { getRandomId } from '@sofie-automation/corelib/dist/lib' import { CoreSystem } from '../collections' +import { useEffect } from 'react' export class ConnectionStatusNotifier extends WithManagedTracker { private _notificationList: NotificationList @@ -233,30 +231,16 @@ function createSystemNotification(cs: ICoreSystem | undefined): Notification | u return undefined } -interface IProps {} -interface IState { - dismissed: boolean -} - -export const ConnectionStatusNotification = withTranslation()( - class ConnectionStatusNotification extends React.Component, IState> { - private notifier: ConnectionStatusNotifier | undefined - - constructor(props: Translated) { - super(props) - } +export function ConnectionStatusNotification(): JSX.Element { + const { t } = useTranslation() - componentDidMount(): void { - this.notifier = new ConnectionStatusNotifier(this.props.t) - } + useEffect(() => { + const notifier = new ConnectionStatusNotifier(t) - componentWillUnmount(): void { - if (this.notifier) this.notifier.stop() + return () => { + notifier.stop() } + }, [t]) - render(): JSX.Element { - // this.props.connected - return - } - } -) + return +} diff --git a/packages/webui/src/client/lib/notifications/NotificationCenterPanel.tsx b/packages/webui/src/client/lib/notifications/NotificationCenterPanel.tsx index 4938b40edc..339f689172 100644 --- a/packages/webui/src/client/lib/notifications/NotificationCenterPanel.tsx +++ b/packages/webui/src/client/lib/notifications/NotificationCenterPanel.tsx @@ -455,9 +455,6 @@ export const NotificationCenterPopUps = translateWithTracker (
@@ -486,9 +483,6 @@ interface IToggleProps { /** * A button for with a count of notifications in the Notification Center - * @export - * @class NotificationCenterPanelToggle - * @extends React.Component */ export function NotificationCenterPanelToggle({ className, diff --git a/packages/webui/src/client/ui/ClockView/Timediff.tsx b/packages/webui/src/client/ui/ClockView/Timediff.tsx index 51f96308a5..55af2adc0b 100644 --- a/packages/webui/src/client/ui/ClockView/Timediff.tsx +++ b/packages/webui/src/client/ui/ClockView/Timediff.tsx @@ -1,22 +1,19 @@ -import * as React from 'react' import ClassNames from 'classnames' import { RundownUtils } from '../../lib/rundown' -export const Timediff = class Timediff extends React.Component<{ time: number }> { - render(): JSX.Element { - const time = -this.props.time - const isNegative = Math.floor(time / 1000) > 0 - const timeString = RundownUtils.formatDiffToTimecode(time, true, false, true, false, true, '', false, true) +export function Timediff({ time: rawTime }: { time: number }): JSX.Element { + const time = -rawTime + const isNegative = Math.floor(time / 1000) > 0 + const timeString = RundownUtils.formatDiffToTimecode(time, true, false, true, false, true, '', false, true) - return ( - -30, - })} - > - {timeString} - - ) - } + return ( + -30, + })} + > + {timeString} + + ) } diff --git a/packages/webui/src/client/ui/PieceIcons/PieceIcon.tsx b/packages/webui/src/client/ui/PieceIcons/PieceIcon.tsx index 1285e5695f..c9f3374159 100644 --- a/packages/webui/src/client/ui/PieceIcons/PieceIcon.tsx +++ b/packages/webui/src/client/ui/PieceIcons/PieceIcon.tsx @@ -6,13 +6,13 @@ import { RemoteContent, EvsContent, } from '@sofie-automation/blueprints-integration' -import CamInputIcon from './Renderers/CamInputIcon' -import VTInputIcon from './Renderers/VTInputIcon' +import { CamInputIcon } from './Renderers/CamInputIcon' +import { VTInputIcon } from './Renderers/VTInputIcon' import SplitInputIcon from './Renderers/SplitInputIcon' -import RemoteInputIcon from './Renderers/RemoteInputIcon' -import LiveSpeakInputIcon from './Renderers/LiveSpeakInputIcon' -import GraphicsInputIcon from './Renderers/GraphicsInputIcon' -import UnknownInputIcon from './Renderers/UnknownInputIcon' +import { RemoteInputIcon } from './Renderers/RemoteInputIcon' +import { LiveSpeakInputIcon } from './Renderers/LiveSpeakInputIcon' +import { GraphicsInputIcon } from './Renderers/GraphicsInputIcon' +import { UnknownInputIcon } from './Renderers/UnknownInputIcon' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { findPieceInstanceToShow, findPieceInstanceToShowFromInstances } from './utils' diff --git a/packages/webui/src/client/ui/PieceIcons/Renderers/CamInputIcon.tsx b/packages/webui/src/client/ui/PieceIcons/Renderers/CamInputIcon.tsx index 305420d58f..88e0126e04 100644 --- a/packages/webui/src/client/ui/PieceIcons/Renderers/CamInputIcon.tsx +++ b/packages/webui/src/client/ui/PieceIcons/Renderers/CamInputIcon.tsx @@ -1,41 +1,43 @@ -import * as React from 'react' - // @todo: use dynamic data for camera number -export default class CamInputIcon extends React.Component<{ inputIndex?: string; abbreviation?: string }> { - render(): JSX.Element { - return ( - - - + + + - - {this.props.abbreviation ? this.props.abbreviation : 'C'} - - {this.props.inputIndex !== undefined ? this.props.inputIndex : ''} - + {abbreviation ? abbreviation : 'C'} + + {inputIndex !== undefined ? inputIndex : ''} - - - ) - } + + + + ) } diff --git a/packages/webui/src/client/ui/PieceIcons/Renderers/GraphicsInputIcon.tsx b/packages/webui/src/client/ui/PieceIcons/Renderers/GraphicsInputIcon.tsx index 582ada7e26..969db38be2 100644 --- a/packages/webui/src/client/ui/PieceIcons/Renderers/GraphicsInputIcon.tsx +++ b/packages/webui/src/client/ui/PieceIcons/Renderers/GraphicsInputIcon.tsx @@ -1,33 +1,30 @@ -import * as React from 'react' -export default class GraphicsInputIcon extends React.Component<{ abbreviation?: string }> { - render(): JSX.Element { - return ( - - - + + + - - {this.props.abbreviation ? this.props.abbreviation : 'G'} - - - - ) - } + {abbreviation ? abbreviation : 'G'} + + + + ) } diff --git a/packages/webui/src/client/ui/PieceIcons/Renderers/LiveSpeakInputIcon.tsx b/packages/webui/src/client/ui/PieceIcons/Renderers/LiveSpeakInputIcon.tsx index f7fa3635ae..e623d132e1 100644 --- a/packages/webui/src/client/ui/PieceIcons/Renderers/LiveSpeakInputIcon.tsx +++ b/packages/webui/src/client/ui/PieceIcons/Renderers/LiveSpeakInputIcon.tsx @@ -1,39 +1,35 @@ -import * as React from 'react' - -export default class LiveSpeakInputIcon extends React.Component<{ abbreviation?: string }> { - render(): JSX.Element { - return ( - - - - - - - + + + + + + + - - {this.props.abbreviation ? this.props.abbreviation : 'LSK'} - - - - ) - } + {abbreviation ? abbreviation : 'LSK'} + + + + ) } diff --git a/packages/webui/src/client/ui/PieceIcons/Renderers/RemoteInputIcon.tsx b/packages/webui/src/client/ui/PieceIcons/Renderers/RemoteInputIcon.tsx index 6d7058618c..13cdbff73a 100644 --- a/packages/webui/src/client/ui/PieceIcons/Renderers/RemoteInputIcon.tsx +++ b/packages/webui/src/client/ui/PieceIcons/Renderers/RemoteInputIcon.tsx @@ -35,11 +35,17 @@ export function BaseRemoteInputIcon(props: Readonly): JSX.Element { +export function RemoteInputIcon({ + inputIndex, + abbreviation, +}: { + inputIndex?: string + abbreviation?: string +}): JSX.Element { return ( - {props.abbreviation ? props.abbreviation : 'LIVE'} - {props.inputIndex ?? ''} + {abbreviation ? abbreviation : 'LIVE'} + {inputIndex ?? ''} ) } diff --git a/packages/webui/src/client/ui/PieceIcons/Renderers/UnknownInputIcon.tsx b/packages/webui/src/client/ui/PieceIcons/Renderers/UnknownInputIcon.tsx index 0d64a1d642..e78367d200 100644 --- a/packages/webui/src/client/ui/PieceIcons/Renderers/UnknownInputIcon.tsx +++ b/packages/webui/src/client/ui/PieceIcons/Renderers/UnknownInputIcon.tsx @@ -1,34 +1,30 @@ -import * as React from 'react' - -export default class UnknownInputIcon extends React.Component<{ abbreviation?: string }> { - render(): JSX.Element { - return ( - - - + + + - - ? - - - - ) - } + ? + + + + ) } diff --git a/packages/webui/src/client/ui/PieceIcons/Renderers/VTInputIcon.tsx b/packages/webui/src/client/ui/PieceIcons/Renderers/VTInputIcon.tsx index 4d701d6e2a..d44ffe0311 100644 --- a/packages/webui/src/client/ui/PieceIcons/Renderers/VTInputIcon.tsx +++ b/packages/webui/src/client/ui/PieceIcons/Renderers/VTInputIcon.tsx @@ -1,34 +1,30 @@ -import * as React from 'react' - -export default class VTInputIcon extends React.Component<{ abbreviation?: string }> { - render(): JSX.Element { - return ( - - - + + + - - {this.props.abbreviation ? this.props.abbreviation : 'VT'} - - - - ) - } + {abbreviation ? abbreviation : 'VT'} + + + + ) } diff --git a/packages/webui/src/client/ui/RundownView.tsx b/packages/webui/src/client/ui/RundownView.tsx index 9210c801db..a95fcefdb1 100644 --- a/packages/webui/src/client/ui/RundownView.tsx +++ b/packages/webui/src/client/ui/RundownView.tsx @@ -12,7 +12,7 @@ import { useTracker, } from '../lib/ReactMeteorData/react-meteor-data' import { VTContent, TSR, NoteSeverity, ISourceLayer } from '@sofie-automation/blueprints-integration' -import { withTranslation, WithTranslation } from 'react-i18next' +import { useTranslation, withTranslation } from 'react-i18next' import timer from 'react-timer-hoc' import * as CoreIcon from '@nrk/core-icons/jsx' import { Spinner } from '../lib/Spinner' @@ -279,75 +279,69 @@ interface ITimingDisplayProps { layout: RundownLayoutRundownHeader | undefined } -const TimingDisplay = withTranslation()( - withTiming()( - class TimingDisplay extends React.Component>> { - render(): JSX.Element | null { - const { t, rundownPlaylist, currentRundown } = this.props - - if (!rundownPlaylist) return null - - const expectedStart = PlaylistTiming.getExpectedStart(rundownPlaylist.timing) - const expectedEnd = PlaylistTiming.getExpectedEnd(rundownPlaylist.timing) - const expectedDuration = PlaylistTiming.getExpectedDuration(rundownPlaylist.timing) - const showEndTiming = - !this.props.timingDurations.rundownsBeforeNextBreak || - !this.props.layout?.showNextBreakTiming || - (this.props.timingDurations.rundownsBeforeNextBreak.length > 0 && - (!this.props.layout?.hideExpectedEndBeforeBreak || - (this.props.timingDurations.breakIsLastRundown && this.props.layout?.lastRundownIsNotBreak))) - const showNextBreakTiming = - rundownPlaylist.startedPlayback && - this.props.timingDurations.rundownsBeforeNextBreak?.length && - this.props.layout?.showNextBreakTiming && - !(this.props.timingDurations.breakIsLastRundown && this.props.layout.lastRundownIsNotBreak) - - return ( -
- - - - {rundownPlaylist.currentPartInfo && ( - - - - {rundownPlaylist.holdState && rundownPlaylist.holdState !== RundownHoldState.COMPLETE ? ( -
{t('Hold')}
- ) : null} -
- )} - {showEndTiming ? ( - - ) : null} - {showNextBreakTiming ? ( - - ) : null} -
- ) - } - } +const TimingDisplay = withTiming()(function TimingDisplay({ + rundownPlaylist, + currentRundown, + rundownCount, + layout, + timingDurations, +}: WithTiming): JSX.Element | null { + const { t } = useTranslation() + + if (!rundownPlaylist) return null + + const expectedStart = PlaylistTiming.getExpectedStart(rundownPlaylist.timing) + const expectedEnd = PlaylistTiming.getExpectedEnd(rundownPlaylist.timing) + const expectedDuration = PlaylistTiming.getExpectedDuration(rundownPlaylist.timing) + const showEndTiming = + !timingDurations.rundownsBeforeNextBreak || + !layout?.showNextBreakTiming || + (timingDurations.rundownsBeforeNextBreak.length > 0 && + (!layout?.hideExpectedEndBeforeBreak || (timingDurations.breakIsLastRundown && layout?.lastRundownIsNotBreak))) + const showNextBreakTiming = + rundownPlaylist.startedPlayback && + timingDurations.rundownsBeforeNextBreak?.length && + layout?.showNextBreakTiming && + !(timingDurations.breakIsLastRundown && layout.lastRundownIsNotBreak) + + return ( +
+ + + + {rundownPlaylist.currentPartInfo && ( + + + + {rundownPlaylist.holdState && rundownPlaylist.holdState !== RundownHoldState.COMPLETE ? ( +
{t('Hold')}
+ ) : null} +
+ )} + {showEndTiming ? ( + + ) : null} + {showNextBreakTiming ? ( + + ) : null} +
) -) +}) interface IRundownHeaderProps { playlist: DBRundownPlaylist diff --git a/packages/webui/src/client/ui/RundownView/PlaylistLoopingHeader.tsx b/packages/webui/src/client/ui/RundownView/PlaylistLoopingHeader.tsx index 97ad4e434f..673844e49a 100644 --- a/packages/webui/src/client/ui/RundownView/PlaylistLoopingHeader.tsx +++ b/packages/webui/src/client/ui/RundownView/PlaylistLoopingHeader.tsx @@ -1,7 +1,6 @@ import React from 'react' import classNames from 'classnames' -import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData' -import { withTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next' import Moment from 'react-moment' import { LoopingIcon } from '../../lib/ui/icons/looping' import { WithTiming, withTiming } from './RundownTiming/withTiming' @@ -44,10 +43,9 @@ interface ILoopingHeaderProps { multiRundown?: boolean showCountdowns?: boolean } -export const PlaylistLoopingHeader = withTranslation()(function PlaylistLoopingHeader( - props: Translated -) { - const { t, position, multiRundown, showCountdowns } = props +export function PlaylistLoopingHeader({ position, multiRundown, showCountdowns }: ILoopingHeaderProps): JSX.Element { + const { t } = useTranslation() + return (
) -}) +} diff --git a/packages/webui/src/client/ui/RundownView/RundownDividerHeader.tsx b/packages/webui/src/client/ui/RundownView/RundownDividerHeader.tsx index 3c372db734..fb2a11951f 100644 --- a/packages/webui/src/client/ui/RundownView/RundownDividerHeader.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownDividerHeader.tsx @@ -1,9 +1,8 @@ import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData' import Moment from 'react-moment' import { TimingDataResolution, TimingTickResolution, withTiming, WithTiming } from './RundownTiming/withTiming' import { RundownUtils } from '../../lib/rundown' -import { withTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' @@ -14,50 +13,41 @@ interface IProps { const QUATER_DAY = 6 * 60 * 60 * 1000 +interface MarkerCountdownProps { + markerTimestamp: number | undefined + className?: string | undefined +} + /** * This is a countdown to the rundown's Expected Start or Expected End time. It shows nothing if the expectedStart is undefined * or the time to Expected Start/End from now is larger than 6 hours. */ -const MarkerCountdownText = withTranslation()( - withTiming< - Translated<{ - markerTimestamp: number | undefined - className?: string | undefined - }>, - {} - >({ - filter: 'currentTime', - tickResolution: TimingTickResolution.Low, - dataResolution: TimingDataResolution.Synced, - })(function MarkerCountdown( - props: Translated< - WithTiming<{ - markerTimestamp: number | undefined - className?: string | undefined - }> - > - ) { - const { t } = props - if (props.markerTimestamp === undefined) return null +const MarkerCountdownText = withTiming({ + filter: 'currentTime', + tickResolution: TimingTickResolution.Low, + dataResolution: TimingDataResolution.Synced, +})(function MarkerCountdown(props: WithTiming) { + const { t } = useTranslation() - const time = props.markerTimestamp - (props.timingDurations.currentTime || 0) + if (props.markerTimestamp === undefined) return null - if (time < QUATER_DAY) { - return ( - - {time > 0 - ? t('(in: {{time}})', { - time: RundownUtils.formatDiffToTimecode(time, false, true, true, true, true), - }) - : t('({{time}} ago)', { - time: RundownUtils.formatDiffToTimecode(time, false, true, true, true, true), - })} - - ) - } - return null - }) -) + const time = props.markerTimestamp - (props.timingDurations.currentTime || 0) + + if (time < QUATER_DAY) { + return ( + + {time > 0 + ? t('(in: {{time}})', { + time: RundownUtils.formatDiffToTimecode(time, false, true, true, true, true), + }) + : t('({{time}} ago)', { + time: RundownUtils.formatDiffToTimecode(time, false, true, true, true, true), + })} + + ) + } + return null +}) /** * This is a component for showing the title of the rundown, it's expectedStart and expectedDuration and @@ -67,8 +57,9 @@ const MarkerCountdownText = withTranslation()( * * The component should be minimally reactive. */ -export const RundownDividerHeader = withTranslation()(function RundownDividerHeader(props: Translated) { - const { t, rundown, playlist } = props +export function RundownDividerHeader({ rundown, playlist }: IProps): JSX.Element { + const { t } = useTranslation() + const expectedStart = PlaylistTiming.getExpectedStart(rundown.timing) const expectedDuration = PlaylistTiming.getExpectedDuration(rundown.timing) const expectedEnd = PlaylistTiming.getExpectedEnd(rundown.timing) @@ -120,4 +111,4 @@ export const RundownDividerHeader = withTranslation()(function RundownDividerHea ) : null}
) -}) +} diff --git a/packages/webui/src/client/ui/RundownView/RundownTiming/PlaylistEndTiming.tsx b/packages/webui/src/client/ui/RundownView/RundownTiming/PlaylistEndTiming.tsx index d48b6b22b8..fc7d0f1790 100644 --- a/packages/webui/src/client/ui/RundownView/RundownTiming/PlaylistEndTiming.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownTiming/PlaylistEndTiming.tsx @@ -1,8 +1,7 @@ import React from 'react' -import { WithTranslation, withTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next' import Moment from 'react-moment' import { getCurrentTime } from '../../../lib/systemTime' -import { Translated } from '../../../lib/ReactMeteorData/ReactMeteorData' import { RundownUtils } from '../../../lib/rundown' import { withTiming, WithTiming } from './withTiming' import ClassNames from 'classnames' @@ -22,116 +21,95 @@ interface IEndTimingProps { hidePlannedEnd?: boolean hideCountdown?: boolean hideDiff?: boolean - rundownCount: number } -export const PlaylistEndTiming = withTranslation()( - withTiming()( - class PlaylistEndTiming extends React.Component>> { - render(): JSX.Element { - const { t } = this.props - const { rundownPlaylist, expectedStart, expectedEnd, expectedDuration } = this.props +export const PlaylistEndTiming = withTiming()(function PlaylistEndTiming({ + rundownPlaylist, + loop, + expectedStart, + expectedDuration, + expectedEnd, + endLabel, + hidePlannedEndLabel, + hideDiffLabel, + hidePlannedEnd, + hideCountdown, + hideDiff, + timingDurations, +}: WithTiming): JSX.Element { + const { t } = useTranslation() - const overUnderClock = getPlaylistTimingDiff(rundownPlaylist, this.props.timingDurations) ?? 0 - const now = this.props.timingDurations.currentTime ?? getCurrentTime() + const overUnderClock = getPlaylistTimingDiff(rundownPlaylist, timingDurations) ?? 0 + const now = timingDurations.currentTime ?? getCurrentTime() - return ( - - {!this.props.hidePlannedEnd ? ( - this.props.expectedEnd ? ( - !rundownPlaylist.startedPlayback ? ( - - {!this.props.hidePlannedEndLabel && ( - {this.props.endLabel ?? t('Planned End')} - )} - - - ) : ( - - {!this.props.hidePlannedEndLabel && ( - {this.props.endLabel ?? t('Expected End')} - )} - - - ) - ) : this.props.timingDurations ? ( - isLoopRunning(this.props.rundownPlaylist) ? ( - this.props.timingDurations.partCountdown && - rundownPlaylist.activationId && - rundownPlaylist.currentPartInfo ? ( - - {!this.props.hidePlannedEndLabel && ( - {t('Next Loop at')} - )} - - - ) : null - ) : ( - - {!this.props.hidePlannedEndLabel && ( - {this.props.endLabel ?? t('Expected End')} - )} - - - ) - ) : null - ) : null} - {!this.props.loop && - !this.props.hideCountdown && - (expectedEnd ? ( - - {RundownUtils.formatDiffToTimecode(now - expectedEnd, true, true, true)} - - ) : expectedStart && expectedDuration ? ( - - {RundownUtils.formatDiffToTimecode( - getCurrentTime() - (expectedStart + expectedDuration), - true, - true, - true - )} - - ) : null)} - {!this.props.hideDiff ? ( - this.props.timingDurations ? ( - = 0, - })} - role="timer" - > - {!this.props.hideDiffLabel && {t('Diff')}} - {RundownUtils.formatDiffToTimecode( - overUnderClock, - true, - false, - true, - true, - true, - undefined, - true, - true - )} - - ) : null - ) : null} - - ) - } - } + return ( + + {!hidePlannedEnd ? ( + expectedEnd ? ( + !rundownPlaylist.startedPlayback ? ( + + {!hidePlannedEndLabel && {endLabel ?? t('Planned End')}} + + + ) : ( + + {!hidePlannedEndLabel && ( + {endLabel ?? t('Expected End')} + )} + + + ) + ) : timingDurations ? ( + isLoopRunning(rundownPlaylist) ? ( + timingDurations.partCountdown && rundownPlaylist.activationId && rundownPlaylist.currentPartInfo ? ( + + {!hidePlannedEndLabel && {t('Next Loop at')}} + + + ) : null + ) : ( + + {!hidePlannedEndLabel && ( + {endLabel ?? t('Expected End')} + )} + + + ) + ) : null + ) : null} + {!loop && + !hideCountdown && + (expectedEnd ? ( + + {RundownUtils.formatDiffToTimecode(now - expectedEnd, true, true, true)} + + ) : expectedStart && expectedDuration ? ( + + {RundownUtils.formatDiffToTimecode(getCurrentTime() - (expectedStart + expectedDuration), true, true, true)} + + ) : null)} + {!hideDiff ? ( + timingDurations ? ( + = 0, + })} + role="timer" + > + {!hideDiffLabel && {t('Diff')}} + {RundownUtils.formatDiffToTimecode(overUnderClock, true, false, true, true, true, undefined, true, true)} + + ) : null + ) : null} + ) -) +}) diff --git a/packages/webui/src/client/ui/RundownView/RundownTiming/PlaylistStartTiming.tsx b/packages/webui/src/client/ui/RundownView/RundownTiming/PlaylistStartTiming.tsx index 9acf15cbd2..aca0e98cff 100644 --- a/packages/webui/src/client/ui/RundownView/RundownTiming/PlaylistStartTiming.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownTiming/PlaylistStartTiming.tsx @@ -1,7 +1,6 @@ import React from 'react' -import { WithTranslation, withTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next' import Moment from 'react-moment' -import { Translated } from '../../../lib/ReactMeteorData/ReactMeteorData' import { withTiming, WithTiming } from './withTiming' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { RundownUtils } from '../../../lib/rundown' @@ -9,70 +8,70 @@ import { getCurrentTime } from '../../../lib/systemTime' import ClassNames from 'classnames' import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' -interface IEndTimingProps { +interface IStartTimingProps { rundownPlaylist: DBRundownPlaylist hidePlannedStart?: boolean hideDiff?: boolean plannedStartText?: string } -export const PlaylistStartTiming = withTranslation()( - withTiming()( - class PlaylistStartTiming extends React.Component>> { - render(): JSX.Element { - const { t, rundownPlaylist } = this.props - const playlistExpectedStart = PlaylistTiming.getExpectedStart(rundownPlaylist.timing) - const playlistExpectedEnd = PlaylistTiming.getExpectedEnd(rundownPlaylist.timing) - const playlistExpectedDuration = PlaylistTiming.getExpectedDuration(rundownPlaylist.timing) - const expectedStart = playlistExpectedStart - ? playlistExpectedStart - : playlistExpectedDuration && playlistExpectedEnd - ? playlistExpectedEnd - playlistExpectedDuration - : undefined +export const PlaylistStartTiming = withTiming()(function PlaylistStartTiming({ + rundownPlaylist, + hidePlannedStart, + hideDiff, + plannedStartText, +}: WithTiming): JSX.Element { + const { t } = useTranslation() - return ( - - {!this.props.hidePlannedStart && - (rundownPlaylist.startedPlayback && rundownPlaylist.activationId && !rundownPlaylist.rehearsal ? ( - - {t('Started')} - - - ) : playlistExpectedStart ? ( - - {this.props.plannedStartText || t('Planned Start')} - - - ) : playlistExpectedEnd && playlistExpectedDuration ? ( - - {this.props.plannedStartText || t('Expected Start')} - - - ) : null)} - {!this.props.hideDiff && expectedStart && ( - expectedStart, - light: getCurrentTime() <= expectedStart, - })} - role="timer" - > - {t('Diff')} - {rundownPlaylist.startedPlayback - ? RundownUtils.formatDiffToTimecode( - rundownPlaylist.startedPlayback - expectedStart, - true, - false, - true, - true, - true - ) - : RundownUtils.formatDiffToTimecode(getCurrentTime() - expectedStart, true, false, true, true, true)} - - )} - - ) - } - } + const playlistExpectedStart = PlaylistTiming.getExpectedStart(rundownPlaylist.timing) + const playlistExpectedEnd = PlaylistTiming.getExpectedEnd(rundownPlaylist.timing) + const playlistExpectedDuration = PlaylistTiming.getExpectedDuration(rundownPlaylist.timing) + const expectedStart = playlistExpectedStart + ? playlistExpectedStart + : playlistExpectedDuration && playlistExpectedEnd + ? playlistExpectedEnd - playlistExpectedDuration + : undefined + + return ( + + {!hidePlannedStart && + (rundownPlaylist.startedPlayback && rundownPlaylist.activationId && !rundownPlaylist.rehearsal ? ( + + {t('Started')} + + + ) : playlistExpectedStart ? ( + + {plannedStartText || t('Planned Start')} + + + ) : playlistExpectedEnd && playlistExpectedDuration ? ( + + {plannedStartText || t('Expected Start')} + + + ) : null)} + {!hideDiff && expectedStart && ( + expectedStart, + light: getCurrentTime() <= expectedStart, + })} + role="timer" + > + {t('Diff')} + {rundownPlaylist.startedPlayback + ? RundownUtils.formatDiffToTimecode( + rundownPlaylist.startedPlayback - expectedStart, + true, + false, + true, + true, + true + ) + : RundownUtils.formatDiffToTimecode(getCurrentTime() - expectedStart, true, false, true, true, true)} + + )} + ) -) +}) diff --git a/packages/webui/src/client/ui/RundownView/RundownTiming/RundownName.tsx b/packages/webui/src/client/ui/RundownView/RundownTiming/RundownName.tsx index 08dc97104d..ffcb578b9d 100644 --- a/packages/webui/src/client/ui/RundownView/RundownTiming/RundownName.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownTiming/RundownName.tsx @@ -1,6 +1,4 @@ -import React from 'react' -import { WithTranslation, withTranslation } from 'react-i18next' -import { Translated } from '../../../lib/ReactMeteorData/ReactMeteorData' +import { useTranslation } from 'react-i18next' import { withTiming, WithTiming } from './withTiming' import ClassNames from 'classnames' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' @@ -18,80 +16,78 @@ interface IRundownNameProps { hideDiff?: boolean } -export const RundownName = withTranslation()( - withTiming()( - class RundownName extends React.Component>> { - render(): JSX.Element { - const { rundownPlaylist, currentRundown, rundownCount, t } = this.props - const expectedStart = PlaylistTiming.getExpectedStart(rundownPlaylist.timing) - const isPlaylistLooping = isLoopDefined(rundownPlaylist) - return ( -
expectedStart, - })} - > - {currentRundown && (rundownPlaylist.name !== currentRundown.name || rundownCount > 1) ? ( -

- {isPlaylistLooping && } {currentRundown.name} {rundownPlaylist.name} -

- ) : ( -

- {isPlaylistLooping && } {rundownPlaylist.name} -

- )} - {!this.props.hideDiff && - rundownPlaylist.startedPlayback && - rundownPlaylist.activationId && - !rundownPlaylist.rehearsal - ? expectedStart && - RundownUtils.formatDiffToTimecode( - rundownPlaylist.startedPlayback - expectedStart, - true, - false, - true, - true, - true - ) - : expectedStart && - RundownUtils.formatDiffToTimecode(getCurrentTime() - expectedStart, true, false, true, true, true)} -
- ) - } - } +export const RundownName = withTiming()(function RundownName({ + rundownPlaylist, + currentRundown, + rundownCount, + hideDiff, +}: WithTiming): JSX.Element { + const { t } = useTranslation() + + const expectedStart = PlaylistTiming.getExpectedStart(rundownPlaylist.timing) + const isPlaylistLooping = isLoopDefined(rundownPlaylist) + + return ( +
expectedStart, + })} + > + {currentRundown && (rundownPlaylist.name !== currentRundown.name || rundownCount > 1) ? ( +

+ {isPlaylistLooping && } {currentRundown.name} {rundownPlaylist.name} +

+ ) : ( +

+ {isPlaylistLooping && } {rundownPlaylist.name} +

+ )} + {!hideDiff && rundownPlaylist.startedPlayback && rundownPlaylist.activationId && !rundownPlaylist.rehearsal + ? expectedStart && + RundownUtils.formatDiffToTimecode( + rundownPlaylist.startedPlayback - expectedStart, + true, + false, + true, + true, + true + ) + : expectedStart && + RundownUtils.formatDiffToTimecode(getCurrentTime() - expectedStart, true, false, true, true, true)} +
) -) +}) diff --git a/packages/webui/src/client/ui/RundownView/RundownTiming/TimeOfDay.tsx b/packages/webui/src/client/ui/RundownView/RundownTiming/TimeOfDay.tsx index bbbdd0ed2f..c571410ce7 100644 --- a/packages/webui/src/client/ui/RundownView/RundownTiming/TimeOfDay.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownTiming/TimeOfDay.tsx @@ -1,14 +1,10 @@ -import { WithTranslation, withTranslation } from 'react-i18next' -import { Translated } from '../../../lib/ReactMeteorData/ReactMeteorData' import { withTiming, WithTiming } from './withTiming' import Moment from 'react-moment' -export const TimeOfDay = withTranslation()( - withTiming()(function RundownName(props: Translated>) { - return ( - - - - ) - }) -) +export const TimeOfDay = withTiming<{}, {}>()(function TimeOfDay({ timingDurations }: WithTiming<{}>) { + return ( + + + + ) +}) diff --git a/packages/webui/src/client/ui/SegmentTimeline/BreakSegment.tsx b/packages/webui/src/client/ui/SegmentTimeline/BreakSegment.tsx index 62adc8a71e..ebb47cb071 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/BreakSegment.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/BreakSegment.tsx @@ -1,7 +1,5 @@ -import React from 'react' -import { WithTranslation, withTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next' import Moment from 'react-moment' -import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData' import { RundownUtils } from '../../lib/rundown' import { WithTiming, withTiming } from '../RundownView/RundownTiming/withTiming' @@ -9,37 +7,29 @@ interface IProps { breakTime: number | undefined } -class BreakSegmentInner extends React.Component>> { - constructor(props: Translated>) { - super(props) - } +function BreakSegmentInner({ breakTime, timingDurations }: WithTiming): JSX.Element { + const { t } = useTranslation() - render(): JSX.Element { - const { t } = this.props - const displayTimecode = - this.props.breakTime && this.props.timingDurations.currentTime - ? this.props.breakTime - this.props.timingDurations.currentTime - : undefined + const displayTimecode = breakTime && timingDurations.currentTime ? breakTime - timingDurations.currentTime : undefined - return ( -
-
-

- {this.props.breakTime && }  - {t('BREAK')} -

-
- {displayTimecode && ( -
- {t('Break In')} - - {RundownUtils.formatDiffToTimecode(displayTimecode, false, undefined, undefined, undefined, true)} - -
- )} + return ( +
+
+

+ {breakTime && }  + {t('BREAK')} +

- ) - } + {displayTimecode && ( +
+ {t('Break In')} + + {RundownUtils.formatDiffToTimecode(displayTimecode, false, undefined, undefined, undefined, true)} + +
+ )} +
+ ) } -export const BreakSegment = withTranslation()(withTiming()(BreakSegmentInner)) +export const BreakSegment = withTiming()(BreakSegmentInner) diff --git a/packages/webui/src/client/ui/Shelf/EndWordsPanel.tsx b/packages/webui/src/client/ui/Shelf/EndWordsPanel.tsx index 6c5a695856..662793b780 100644 --- a/packages/webui/src/client/ui/Shelf/EndWordsPanel.tsx +++ b/packages/webui/src/client/ui/Shelf/EndWordsPanel.tsx @@ -1,4 +1,3 @@ -import * as React from 'react' import * as _ from 'underscore' import ClassNames from 'classnames' import { @@ -8,7 +7,7 @@ import { } from '@sofie-automation/meteor-lib/dist/collections/RundownLayouts' import { RundownLayoutsAPI } from '../../lib/rundownLayouts' import { dashboardElementStyle } from './DashboardPanel' -import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData' +import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { ScriptContent } from '@sofie-automation/blueprints-integration' @@ -17,65 +16,55 @@ import { getScriptPreview } from '../../lib/ui/scriptPreview' import { UIShowStyleBase } from '@sofie-automation/meteor-lib/dist/api/showStyles' import { PieceInstances } from '../../collections' import { ReadonlyDeep } from 'type-fest' +import { useTranslation } from 'react-i18next' interface IEndsWordsPanelProps { - visible?: boolean layout: RundownLayoutBase panel: RundownLayoutEndWords playlist: DBRundownPlaylist showStyleBase: UIShowStyleBase } -interface IEndsWordsPanelTrackedProps { - livePieceInstance?: PieceInstance -} - -interface IState {} - -class EndWordsPanelInner extends React.Component< - Translated, - IState -> { - render(): JSX.Element { - const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout) +export function EndWordsPanel({ layout, panel, playlist, showStyleBase }: IEndsWordsPanelProps): JSX.Element { + const { t } = useTranslation() - const { t, livePieceInstance, panel } = this.props - const content = livePieceInstance?.piece.content as Partial | undefined + const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(layout) - const { endOfScript } = getScriptPreview(content?.fullScript || '') + const livePieceInstance = useTracker( + () => getPieceWithScript(playlist, showStyleBase, panel), + [playlist, showStyleBase, panel] + ) - return ( -
-
- {!this.props.panel.hideLabel && {t('End Words')}} - ‎{endOfScript}‎ -
+ const content = livePieceInstance?.piece.content as Partial | undefined + + const { endOfScript } = getScriptPreview(content?.fullScript || '') + + return ( +
+
+ {!panel.hideLabel && {t('End Words')}} + ‎{endOfScript}‎
- ) - } +
+ ) } -export const EndWordsPanel = translateWithTracker( - (props: IEndsWordsPanelProps) => { - return { livePieceInstance: getPieceWithScript(props) } - }, - (_data, props: IEndsWordsPanelProps, nextProps: IEndsWordsPanelProps) => { - return !_.isEqual(props, nextProps) - } -)(EndWordsPanelInner) - -function getPieceWithScript(props: IEndsWordsPanelProps): PieceInstance | undefined { - const currentPartInstanceId = props.playlist.currentPartInfo?.partInstanceId +function getPieceWithScript( + playlist: DBRundownPlaylist, + showStyleBase: UIShowStyleBase, + panel: RundownLayoutEndWords +): PieceInstance | undefined { + const currentPartInstanceId = playlist.currentPartInfo?.partInstanceId const unfinishedPiecesIncludingFinishedPiecesWhereEndTimeHaveNotBeenSet = getUnfinishedPieceInstancesReactive( - props.playlist, - props.showStyleBase + playlist, + showStyleBase ) const highestStartedPlayback = unfinishedPiecesIncludingFinishedPiecesWhereEndTimeHaveNotBeenSet.reduce( @@ -90,7 +79,7 @@ function getPieceWithScript(props: IEndsWordsPanelProps): PieceInstance | undefi ) const activeLayers = unfinishedPieces.map((p) => p.piece.sourceLayerId) - const hasAdditionalLayer: boolean = props.panel.additionalLayers?.some((s) => activeLayers.includes(s)) ?? false + const hasAdditionalLayer: boolean = panel.additionalLayers?.some((s) => activeLayers.includes(s)) ?? false if (!hasAdditionalLayer) { return undefined @@ -100,15 +89,15 @@ function getPieceWithScript(props: IEndsWordsPanelProps): PieceInstance | undefi const piecesInPart: PieceInstance[] = currentPartInstanceId ? PieceInstances.find({ partInstanceId: currentPartInstanceId, - playlistActivationId: props.playlist.activationId, + playlistActivationId: playlist.activationId, }).fetch() : [] - return props.panel.requiredLayerIds && props.panel.requiredLayerIds.length + return panel.requiredLayerIds && panel.requiredLayerIds.length ? piecesInPart.find((piece: PieceInstance) => { return ( - (props.panel.requiredLayerIds || []).indexOf(piece.piece.sourceLayerId) !== -1 && - piece.partInstanceId === props.playlist.currentPartInfo?.partInstanceId + (panel.requiredLayerIds || []).indexOf(piece.piece.sourceLayerId) !== -1 && + piece.partInstanceId === playlist.currentPartInfo?.partInstanceId ) }) : undefined diff --git a/packages/webui/src/client/ui/Shelf/Inspector/ItemRenderers/ItemRendererFactory.ts b/packages/webui/src/client/ui/Shelf/Inspector/ItemRenderers/ItemRendererFactory.ts index 88755ddf81..94a77dc8de 100644 --- a/packages/webui/src/client/ui/Shelf/Inspector/ItemRenderers/ItemRendererFactory.ts +++ b/packages/webui/src/client/ui/Shelf/Inspector/ItemRenderers/ItemRendererFactory.ts @@ -1,6 +1,6 @@ import * as React from 'react' import DefaultItemRenderer from './DefaultItemRenderer' -import NoraItemRenderer, { isNoraItem } from './NoraItemRenderer' +import { NoraItemRenderer, isNoraItem } from './NoraItemRenderer' import ActionItemRenderer, { isActionItem } from './ActionItemRenderer' import { PieceUi } from '../../../SegmentTimeline/SegmentTimelineContainer' diff --git a/packages/webui/src/client/ui/Shelf/Inspector/ItemRenderers/NoraItemRenderer.tsx b/packages/webui/src/client/ui/Shelf/Inspector/ItemRenderers/NoraItemRenderer.tsx index f24dafd760..9c77f983e3 100644 --- a/packages/webui/src/client/ui/Shelf/Inspector/ItemRenderers/NoraItemRenderer.tsx +++ b/packages/webui/src/client/ui/Shelf/Inspector/ItemRenderers/NoraItemRenderer.tsx @@ -1,15 +1,15 @@ -import * as React from 'react' import { NoraContent } from '@sofie-automation/blueprints-integration' import { IModalAttributes, Modal } from '../../../../lib/ui/containers/modals/Modal' import { NoraItemEditor } from './NoraItemEditor' import { PieceUi } from '../../../SegmentTimeline/SegmentTimelineContainer' import { RundownUtils } from '../../../../lib/rundown' -import { withTranslation, WithTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next' import InspectorTitle from './InspectorTitle' import { ErrorBoundary } from '../../../../lib/ErrorBoundary' import { IAdLibListItem } from '../../AdLibListItem' import { UIShowStyleBase } from '@sofie-automation/meteor-lib/dist/api/showStyles' import { UIStudio } from '@sofie-automation/meteor-lib/dist/api/studios' +import { useState } from 'react' export { isNoraItem } @@ -19,66 +19,44 @@ interface INoraSuperRendererProps { studio: UIStudio } -interface INoraSuperRendererState { - editMode: boolean -} - -export default withTranslation()( - class NoraItemRenderer extends React.Component { - constructor(props: INoraSuperRendererProps & WithTranslation) { - super(props) - - this.state = { - editMode: false, - } - } - - setEditMode(enabled: boolean) { - this.setState({ editMode: enabled === true }) - } +export function NoraItemRenderer({ studio, showStyleBase, piece }: INoraSuperRendererProps): JSX.Element { + const { t } = useTranslation() - render(): JSX.Element { - const { piece, t } = this.props + const actualPiece = RundownUtils.isAdLibPiece(piece) ? piece : piece.instance.piece - const actualPiece = RundownUtils.isAdLibPiece(piece) ? piece : piece.instance.piece + const [editMode, setEditMode] = useState(false) - const modalProps: IModalAttributes = { - title: actualPiece.name, - show: this.state.editMode, - onDiscard: () => { - this.setEditMode(false) - }, - } - - return ( - - -
-

{actualPiece.name}

-
- -
- - - -
-
- ) - } + const modalProps: IModalAttributes = { + title: actualPiece.name, + show: editMode, + onDiscard: () => { + setEditMode(false) + }, } -) + + return ( + + +
+

{actualPiece.name}

+
+ +
+ + + +
+
+ ) +} function isNoraItem(item: IAdLibListItem | PieceUi): boolean { const content = RundownUtils.isAdLibPiece(item) diff --git a/packages/webui/src/client/ui/Shelf/NextBreakTimingPanel.tsx b/packages/webui/src/client/ui/Shelf/NextBreakTimingPanel.tsx deleted file mode 100644 index db2eb2668a..0000000000 --- a/packages/webui/src/client/ui/Shelf/NextBreakTimingPanel.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import * as React from 'react' -import ClassNames from 'classnames' -import { - DashboardLayoutNextBreakTiming, - RundownLayoutBase, - RundownLayoutNextBreakTiming, -} from '@sofie-automation/meteor-lib/dist/collections/RundownLayouts' -import { RundownLayoutsAPI } from '../../lib/rundownLayouts' -import { dashboardElementStyle } from './DashboardPanel' -import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { withTranslation } from 'react-i18next' -import { NextBreakTiming } from '../RundownView/RundownTiming/NextBreakTiming' - -interface INextBreakTimingPanelProps { - visible?: boolean - layout: RundownLayoutBase - panel: RundownLayoutNextBreakTiming - playlist: DBRundownPlaylist -} - -export class NextBreakTimingPanelInner extends React.Component> { - render(): JSX.Element { - const { panel, layout } = this.props - - const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(layout) - - return ( -
- -
- ) - } -} - -export const NextBreakTimingPanel = withTranslation()(NextBreakTimingPanelInner) diff --git a/packages/webui/src/client/ui/Shelf/NextInfoPanel.tsx b/packages/webui/src/client/ui/Shelf/NextInfoPanel.tsx index f614020cbf..c669e8eacc 100644 --- a/packages/webui/src/client/ui/Shelf/NextInfoPanel.tsx +++ b/packages/webui/src/client/ui/Shelf/NextInfoPanel.tsx @@ -1,4 +1,3 @@ -import * as React from 'react' import * as _ from 'underscore' import ClassNames from 'classnames' import { @@ -7,13 +6,13 @@ import { RundownLayoutNextInfo, } from '@sofie-automation/meteor-lib/dist/collections/RundownLayouts' import { RundownLayoutsAPI } from '../../lib/rundownLayouts' -import { withTracker } from '../../lib/ReactMeteorData/ReactMeteorData' +import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { PartInstance } from '@sofie-automation/meteor-lib/dist/collections/PartInstances' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { dashboardElementStyle } from './DashboardPanel' import { Segments } from '../../collections' import { UIPartInstances } from '../Collections' +import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' interface INextInfoPanelProps { visible?: boolean @@ -22,57 +21,51 @@ interface INextInfoPanelProps { playlist: DBRundownPlaylist } -interface INextInfoPanelTrackedProps { - nextPartInstance?: PartInstance - nextSegment?: DBSegment -} +export function NextInfoPanel({ visible, layout, panel, playlist }: INextInfoPanelProps): JSX.Element { + const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(layout) -export class NextInfoPanelInner extends React.Component { - render(): JSX.Element { - const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout) - const showAny = - !this.props.panel.hideForDynamicallyInsertedParts || this.props.nextPartInstance?.orphaned !== 'adlib-part' - const segmentName = showAny && this.props.panel.showSegmentName && this.props.nextSegment?.name - const partTitle = showAny && this.props.panel.showPartTitle && this.props.nextPartInstance?.part.title - return ( -
-
- {showAny && this.props.panel.name} - {segmentName && {segmentName}} - {partTitle && {partTitle}} -
-
- ) - } -} + const nextPartInstanceId = playlist.nextPartInfo?.partInstanceId + const nextPartInstance = useTracker( + () => + nextPartInstanceId && + (UIPartInstances.findOne(nextPartInstanceId, { + projection: { + segmentId: 1, + orphaned: 1, + part: 1, + }, + }) as Pick), + [nextPartInstanceId] + ) -export const NextInfoPanel = withTracker( - (props: INextInfoPanelProps & INextInfoPanelTrackedProps) => { - let nextPartInstance: PartInstance | undefined = undefined - let nextSegment: DBSegment | undefined = undefined + const nextSegmentId = nextPartInstance?.segmentId + const nextSegment = useTracker( + () => nextSegmentId && (Segments.findOne(nextSegmentId, { projection: { name: 1 } }) as Pick), + [nextSegmentId] + ) - if (props.playlist.nextPartInfo) { - nextPartInstance = UIPartInstances.findOne(props.playlist.nextPartInfo.partInstanceId) - } - if (nextPartInstance) { - nextSegment = Segments.findOne(nextPartInstance.segmentId) - } - return { nextPartInstance, nextSegment } - }, - (_data, props: INextInfoPanelProps, nextProps: INextInfoPanelProps) => { - return !_.isEqual(props, nextProps) - } -)(NextInfoPanelInner) + const showAny = !panel.hideForDynamicallyInsertedParts || nextPartInstance?.orphaned !== 'adlib-part' + const segmentName = showAny && panel.showSegmentName && nextSegment?.name + const partTitle = showAny && panel.showPartTitle && nextPartInstance?.part.title + + return ( +
+
+ {showAny && panel.name} + {segmentName && {segmentName}} + {partTitle && {partTitle}} +
+
+ ) +} diff --git a/packages/webui/src/client/ui/Shelf/PartNamePanel.tsx b/packages/webui/src/client/ui/Shelf/PartNamePanel.tsx index ab8af0d09c..617f854099 100644 --- a/packages/webui/src/client/ui/Shelf/PartNamePanel.tsx +++ b/packages/webui/src/client/ui/Shelf/PartNamePanel.tsx @@ -1,5 +1,3 @@ -import * as React from 'react' -import * as _ from 'underscore' import ClassNames from 'classnames' import { DashboardLayoutPartName, @@ -9,94 +7,74 @@ import { import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { dashboardElementStyle } from './DashboardPanel' import { RundownLayoutsAPI } from '../../lib/rundownLayouts' -import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData' +import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { findPieceInstanceToShowFromInstances, IFoundPieceInstance } from '../PieceIcons/utils' import { pieceIconSupportedLayers } from '../PieceIcons/PieceIcon' import { RundownUtils } from '../../lib/rundown' import { UIShowStyleBase } from '@sofie-automation/meteor-lib/dist/api/showStyles' import { PieceInstances } from '../../collections' import { RundownPlaylistClientUtil } from '../../lib/rundownPlaylistUtil' +import { useTranslation } from 'react-i18next' interface IPartNamePanelProps { - visible?: boolean layout: RundownLayoutBase panel: RundownLayoutPartName playlist: DBRundownPlaylist showStyleBase: UIShowStyleBase } -interface IState {} - interface IPartNamePanelTrackedProps { - name?: string + partName?: string instanceToShow?: IFoundPieceInstance } -class PartNamePanelInner extends React.Component, IState> { - render(): JSX.Element { - const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout) - const { t } = this.props - - const sourceLayerType = this.props.instanceToShow?.sourceLayer?.type - let backgroundSourceLayer = sourceLayerType ? RundownUtils.getSourceLayerClassName(sourceLayerType) : undefined - - if (!backgroundSourceLayer) { - backgroundSourceLayer = '' - } +export function PartNamePanel({ layout, panel, playlist, showStyleBase }: IPartNamePanelProps): JSX.Element { + const { t } = useTranslation() - return ( -
-
- - {this.props.panel.part === 'current' ? t('Current Part') : t('Next Part')} - - {this.props.name} -
-
- ) - } -} + const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(layout) -export const PartNamePanel = translateWithTracker( - (props) => { - const selectedPartInstanceId = - props.panel.part === 'current' - ? props.playlist.currentPartInfo?.partInstanceId - : props.playlist.nextPartInfo?.partInstanceId - let name: string | undefined - let instanceToShow: IFoundPieceInstance | undefined + const selectedPartInstanceId = + panel.part === 'current' ? playlist.currentPartInfo?.partInstanceId : playlist.nextPartInfo?.partInstanceId - if (selectedPartInstanceId) { - const selectedPartInstance = RundownPlaylistClientUtil.getActivePartInstances(props.playlist, { + const { partName, instanceToShow } = useTracker( + () => { + if (!selectedPartInstanceId || !panel.showPieceIconColor) return {} + const selectedPartInstance = RundownPlaylistClientUtil.getActivePartInstances(playlist, { _id: selectedPartInstanceId, })[0] - if (selectedPartInstance && props.panel.showPieceIconColor) { - name = selectedPartInstance.part?.title - const pieceInstances = PieceInstances.find({ partInstanceId: selectedPartInstance._id }).fetch() - instanceToShow = findPieceInstanceToShowFromInstances( - pieceInstances, - props.showStyleBase.sourceLayers, - pieceIconSupportedLayers - ) - } - } + if (!selectedPartInstance) return {} - return { - ...props, - name, - instanceToShow, - } - }, - (_data, props, nextProps) => { - return ( - !_.isEqual(props.panel, nextProps.panel) || - props.playlist.currentPartInfo?.partInstanceId !== nextProps.playlist.currentPartInfo?.partInstanceId || - props.playlist.nextPartInfo?.partInstanceId !== nextProps.playlist.nextPartInfo?.partInstanceId - ) + const partName = selectedPartInstance.part?.title + const pieceInstances = PieceInstances.find({ partInstanceId: selectedPartInstance._id }).fetch() + const instanceToShow = findPieceInstanceToShowFromInstances( + pieceInstances, + showStyleBase.sourceLayers, + pieceIconSupportedLayers + ) + return { partName, instanceToShow } + }, + [panel.showPieceIconColor, playlist._id, showStyleBase.sourceLayers], + {} + ) + + const sourceLayerType = instanceToShow?.sourceLayer?.type + let backgroundSourceLayer = sourceLayerType ? RundownUtils.getSourceLayerClassName(sourceLayerType) : undefined + + if (!backgroundSourceLayer) { + backgroundSourceLayer = '' } -)(PartNamePanelInner) + + return ( +
+
+ {panel.part === 'current' ? t('Current Part') : t('Next Part')} + {partName} +
+
+ ) +} diff --git a/packages/webui/src/client/ui/Shelf/PartTimingPanel.tsx b/packages/webui/src/client/ui/Shelf/PartTimingPanel.tsx index 00e00ddb98..5fcb7240c3 100644 --- a/packages/webui/src/client/ui/Shelf/PartTimingPanel.tsx +++ b/packages/webui/src/client/ui/Shelf/PartTimingPanel.tsx @@ -1,13 +1,10 @@ -import * as React from 'react' -import * as _ from 'underscore' import { DashboardLayoutPartCountDown, RundownLayoutBase, RundownLayoutPartTiming, } from '@sofie-automation/meteor-lib/dist/collections/RundownLayouts' -import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData' +import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { PartInstance } from '@sofie-automation/meteor-lib/dist/collections/PartInstances' import { dashboardElementStyle } from './DashboardPanel' import { RundownLayoutsAPI } from '../../lib/rundownLayouts' import { getAllowSpeaking, getAllowVibrating } from '../../lib/localStorage' @@ -16,9 +13,10 @@ import { CurrentPartElapsed } from '../RundownView/RundownTiming/CurrentPartElap import { getIsFilterActive } from '../../lib/rundownLayouts' import { UIShowStyleBase } from '@sofie-automation/meteor-lib/dist/api/showStyles' import { RundownPlaylistClientUtil } from '../../lib/rundownPlaylistUtil' +import { useTranslation } from 'react-i18next' +import { PartId } from '@sofie-automation/corelib/dist/dataModel/Ids' interface IPartTimingPanelProps { - visible?: boolean layout: RundownLayoutBase panel: RundownLayoutPartTiming playlist: DBRundownPlaylist @@ -26,63 +24,56 @@ interface IPartTimingPanelProps { } interface IPartTimingPanelTrackedProps { - livePart?: PartInstance + livePartId?: PartId active: boolean } -interface IState {} +export function PartTimingPanel({ layout, panel, playlist, showStyleBase }: IPartTimingPanelProps): JSX.Element { + const { t } = useTranslation() -class PartTimingPanelInner extends React.Component< - Translated, - IState -> { - render(): JSX.Element { - const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout) - const { t, panel } = this.props + const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(layout) - return ( -
- - {!panel.hideLabel && ( - - {panel.timingType === 'count_down' ? t('Part Count Down') : t('Part Count Up')} - - )} - {this.props.active && - (panel.timingType === 'count_down' ? ( - - ) : ( - - ))} - -
- ) - } -} + const { active, livePartId } = useTracker( + () => { + if (!playlist.currentPartInfo) return { active: false } + + const livePartId: PartId | undefined = RundownPlaylistClientUtil.getActivePartInstances(playlist, { + _id: playlist.currentPartInfo.partInstanceId, + })[0]?.part?._id -export const PartTimingPanel = translateWithTracker( - (props: IPartTimingPanelProps) => { - if (props.playlist.currentPartInfo) { - const livePart = RundownPlaylistClientUtil.getActivePartInstances(props.playlist, { - _id: props.playlist.currentPartInfo.partInstanceId, - })[0] - const { active } = getIsFilterActive(props.playlist, props.showStyleBase, props.panel) + const { active } = getIsFilterActive(playlist, showStyleBase, panel) - return { active, livePart } - } - return { active: false } - }, - (_data, props: IPartTimingPanelProps, nextProps: IPartTimingPanelProps) => { - return !_.isEqual(props, nextProps) - } -)(PartTimingPanelInner) + return { active, livePartId } + }, + [playlist, showStyleBase, panel], + { active: false } + ) + + return ( +
+ + {!panel.hideLabel && ( + + {panel.timingType === 'count_down' ? t('Part Count Down') : t('Part Count Up')} + + )} + {active && + (panel.timingType === 'count_down' ? ( + + ) : ( + + ))} + +
+ ) +} diff --git a/packages/webui/src/client/ui/Shelf/PlaylistEndTimerPanel.tsx b/packages/webui/src/client/ui/Shelf/PlaylistEndTimerPanel.tsx index 1fa8d6c069..60360eb424 100644 --- a/packages/webui/src/client/ui/Shelf/PlaylistEndTimerPanel.tsx +++ b/packages/webui/src/client/ui/Shelf/PlaylistEndTimerPanel.tsx @@ -41,7 +41,6 @@ export function PlaylistEndTimerPanel({ playlist, panel, layout }: Readonly
) diff --git a/packages/webui/src/client/ui/Shelf/PlaylistNamePanel.tsx b/packages/webui/src/client/ui/Shelf/PlaylistNamePanel.tsx index d8be2534f7..4e55942f16 100644 --- a/packages/webui/src/client/ui/Shelf/PlaylistNamePanel.tsx +++ b/packages/webui/src/client/ui/Shelf/PlaylistNamePanel.tsx @@ -1,4 +1,3 @@ -import * as React from 'react' import ClassNames from 'classnames' import { DashboardLayoutPlaylistName, @@ -8,69 +7,38 @@ import { import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { dashboardElementStyle } from './DashboardPanel' import { RundownLayoutsAPI } from '../../lib/rundownLayouts' -import { withTracker } from '../../lib/ReactMeteorData/ReactMeteorData' -import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' +import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { Rundowns } from '../../collections' -import { PartInstance } from '@sofie-automation/meteor-lib/dist/collections/PartInstances' -import { logger } from '../../lib/logging' -import { RundownPlaylistClientUtil } from '../../lib/rundownPlaylistUtil' interface IPlaylistNamePanelProps { - visible?: boolean layout: RundownLayoutBase panel: RundownLayoutPlaylistName playlist: DBRundownPlaylist } -interface IState {} - -interface IPlaylistNamePanelTrackedProps { - currentRundown?: Rundown -} - -class PlaylistNamePanelInner extends React.Component { - render(): JSX.Element { - const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout) - const { panel } = this.props - - return ( -
-
- {this.props.playlist.name} - {this.props.panel.showCurrentRundownName && this.props.currentRundown && ( - {this.props.currentRundown.name} - )} -
+export function PlaylistNamePanel({ panel, layout, playlist }: IPlaylistNamePanelProps): JSX.Element { + const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(layout) + + const currentRundownId = playlist.currentPartInfo?.rundownId + const currentRundownName = useTracker(() => { + if (!panel.showCurrentRundownName || !currentRundownId) return undefined + const rundown = Rundowns.findOne(currentRundownId, { projection: { name: 1 } }) as Pick + return rundown?.name + }, [currentRundownId, panel.showCurrentRundownName]) + + return ( +
+
+ {playlist.name} + {currentRundownName && {currentRundownName}}
- ) - } +
+ ) } - -export const PlaylistNamePanel = withTracker( - (props: IPlaylistNamePanelProps) => { - if (props.playlist.currentPartInfo) { - const livePart: PartInstance = RundownPlaylistClientUtil.getActivePartInstances(props.playlist, { - _id: props.playlist.currentPartInfo.partInstanceId, - })[0] - if (!livePart) { - logger.warn( - `No PartInstance found for PartInstanceId: ${props.playlist.currentPartInfo.partInstanceId} in Playlist: ${props.playlist._id}` - ) - return {} - } - const currentRundown = Rundowns.findOne({ _id: livePart.rundownId, playlistId: props.playlist._id }) - - return { - currentRundown, - } - } - - return {} - } -)(PlaylistNamePanelInner) diff --git a/packages/webui/src/client/ui/Shelf/SegmentNamePanel.tsx b/packages/webui/src/client/ui/Shelf/SegmentNamePanel.tsx index cd3478378c..8c26a654da 100644 --- a/packages/webui/src/client/ui/Shelf/SegmentNamePanel.tsx +++ b/packages/webui/src/client/ui/Shelf/SegmentNamePanel.tsx @@ -1,4 +1,3 @@ -import * as React from 'react' import ClassNames from 'classnames' import { DashboardLayoutSegmentName, @@ -8,49 +7,41 @@ import { import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { dashboardElementStyle } from './DashboardPanel' import { RundownLayoutsAPI } from '../../lib/rundownLayouts' -import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData' +import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { PartInstance } from '@sofie-automation/meteor-lib/dist/collections/PartInstances' import { RundownPlaylistClientUtil } from '../../lib/rundownPlaylistUtil' +import { useTranslation } from 'react-i18next' interface ISegmentNamePanelProps { - visible?: boolean layout: RundownLayoutBase panel: RundownLayoutSegmentName playlist: DBRundownPlaylist } -interface IState {} +export function SegmentNamePanel({ layout, panel, playlist }: ISegmentNamePanelProps): JSX.Element { + const { t } = useTranslation() -interface ISegmentNamePanelTrackedProps { - name?: string -} + const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(layout) -class SegmentNamePanelInner extends React.Component< - Translated, - IState -> { - render(): JSX.Element { - const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout) - const { t, panel } = this.props + const segmentName = useTracker(() => getSegmentName(panel.segment, playlist), [panel.segment, playlist]) - return ( -
-
- - {this.props.panel.segment === 'current' ? t('Current Segment') : t('Next Segment')} - - {this.props.name} -
+ return ( +
+
+ + {panel.segment === 'current' ? t('Current Segment') : t('Next Segment')} + + {segmentName}
- ) - } +
+ ) } function getSegmentName(selectedSegment: 'current' | 'next', playlist: DBRundownPlaylist): string | undefined { @@ -92,14 +83,3 @@ function getSegmentName(selectedSegment: 'current' | 'next', playlist: DBRundown return nextSegment?.name } } - -export const SegmentNamePanel = translateWithTracker( - (props) => { - const name: string | undefined = getSegmentName(props.panel.segment, props.playlist) - - return { - ...props, - name, - } - } -)(SegmentNamePanelInner) diff --git a/packages/webui/src/client/ui/Shelf/SegmentTimingPanel.tsx b/packages/webui/src/client/ui/Shelf/SegmentTimingPanel.tsx index 898bd6ffb3..1075acf95c 100644 --- a/packages/webui/src/client/ui/Shelf/SegmentTimingPanel.tsx +++ b/packages/webui/src/client/ui/Shelf/SegmentTimingPanel.tsx @@ -1,4 +1,3 @@ -import * as React from 'react' import * as _ from 'underscore' import ClassNames from 'classnames' import { @@ -6,7 +5,7 @@ import { RundownLayoutBase, RundownLayoutSegmentTiming, } from '@sofie-automation/meteor-lib/dist/collections/RundownLayouts' -import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/ReactMeteorData' +import { withTracker } from '../../lib/ReactMeteorData/ReactMeteorData' import { RundownUtils } from '../../lib/rundown' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' @@ -23,6 +22,7 @@ import { UIShowStyleBase } from '@sofie-automation/meteor-lib/dist/api/showStyle import { PartId, RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { RundownPlaylistCollectionUtil } from '../../collections/rundownPlaylistUtil' import { RundownPlaylistClientUtil } from '../../lib/rundownPlaylistUtil' +import { useTranslation } from 'react-i18next' interface ISegmentTimingPanelProps { visible?: boolean @@ -38,49 +38,44 @@ interface ISegmentTimingPanelTrackedProps { active: boolean } -interface IState {} +function SegmentTimingPanelInner({ + layout, + panel, + liveSegment, + parts, + active, +}: ISegmentTimingPanelProps & ISegmentTimingPanelTrackedProps) { + const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(layout) + const { t } = useTranslation() -class SegmentTimingPanelInner extends React.Component< - Translated, - IState -> { - render(): JSX.Element { - const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout) - const { t, panel } = this.props - - return ( -
+ + {!panel.hideLabel && ( + + {panel.timingType === 'count_down' ? t('Segment Count Down') : t('Segment Count Up')} + )} - style={isDashboardLayout ? dashboardElementStyle(this.props.panel as DashboardLayoutSegmentCountDown) : {}} - > - - {!panel.hideLabel && ( - - {panel.timingType === 'count_down' ? t('Segment Count Down') : t('Segment Count Up')} - - )} - {this.props.active && this.props.liveSegment && this.props.parts && ( - - )} - -
- ) - } + {active && liveSegment && parts && ( + + )} + +
+ ) } -export const SegmentTimingPanel = translateWithTracker< - ISegmentTimingPanelProps, - IState, - ISegmentTimingPanelTrackedProps ->( +export const SegmentTimingPanel = withTracker( (props: ISegmentTimingPanelProps) => { if (props.playlist.currentPartInfo) { const livePart = RundownPlaylistClientUtil.getActivePartInstances(props.playlist, { diff --git a/packages/webui/src/client/ui/Status/DebugState.tsx b/packages/webui/src/client/ui/Status/DebugState.tsx index 8641e3269c..3c57d13e79 100644 --- a/packages/webui/src/client/ui/Status/DebugState.tsx +++ b/packages/webui/src/client/ui/Status/DebugState.tsx @@ -1,71 +1,60 @@ -import * as React from 'react' -import { Translated } from '../../lib/ReactMeteorData/react-meteor-data' -import * as reacti18next from 'react-i18next' +import { useTranslation } from 'react-i18next' interface IDebugStateTableProps { debugState: object } -interface IDebugStateTableState {} -export const DebugStateTable = reacti18next.withTranslation()( - class DebugStateTable extends React.Component, IDebugStateTableState> { - constructor(props: Translated) { - super(props) - } +export function DebugStateTable({ debugState }: IDebugStateTableProps): JSX.Element { + const { t } = useTranslation() - private getDebugStateTableBody(debugState: object) { - /** - * Flattens object such that deeply-nested keys are moved to the top-level and are prefixed by - * their parent keys. - * - * # Example - * - * { "key1": { "key2": [ { "key3": "example" } ] } } - * - * becomes - * - * { "key1.key2.0.key3": "example" } - * @param acc Accumulator object, should be passed an empty object to begin - * @param obj Object to recurse - * @param currentKey Current key within the object being recursed (initially blank) - * @returns "Flattened" object - */ - function toDotNotation(acc: any, obj: any, currentKey?: string): object { - for (const key in obj) { - const value = obj[key] - const newKey = currentKey ? currentKey + '.' + key : key // joined key with dot - if (value && typeof value === 'object' && Object.keys(value).length) { - acc = toDotNotation(acc, value, newKey) // it's a nested object, so do it again - } else { - acc[newKey] = value // it's not an object, so set the property - } - } + return ( +
+ + + {getDebugStateTableBody(debugState)} +
+
+ ) +} - return acc +function getDebugStateTableBody(debugState: object) { + /** + * Flattens object such that deeply-nested keys are moved to the top-level and are prefixed by + * their parent keys. + * + * # Example + * + * { "key1": { "key2": [ { "key3": "example" } ] } } + * + * becomes + * + * { "key1.key2.0.key3": "example" } + * @param acc Accumulator object, should be passed an empty object to begin + * @param obj Object to recurse + * @param currentKey Current key within the object being recursed (initially blank) + * @returns "Flattened" object + */ + function toDotNotation(acc: any, obj: any, currentKey?: string): object { + for (const key in obj) { + const value = obj[key] + const newKey = currentKey ? currentKey + '.' + key : key // joined key with dot + if (value && typeof value === 'object' && Object.keys(value).length) { + acc = toDotNotation(acc, value, newKey) // it's a nested object, so do it again + } else { + acc[newKey] = value // it's not an object, so set the property } - - const objectInDotNotation = toDotNotation({}, debugState) - return Object.entries(objectInDotNotation).map(([key, value]) => { - return ( - - {key} - {JSON.stringify(value)} - - ) - }) } - render(): JSX.Element { - const { t } = this.props - - return ( -
- - - {this.getDebugStateTableBody(this.props.debugState)} -
-
- ) - } + return acc } -) + + const objectInDotNotation = toDotNotation({}, debugState) + return Object.entries(objectInDotNotation).map(([key, value]) => { + return ( + + {key} + {JSON.stringify(value)} + + ) + }) +} From 8dc29c9d5b84895241c5fd6897dc8e56f9dcb3c0 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 18 Dec 2024 14:22:11 +0000 Subject: [PATCH 2/3] chore: refactor EditAttribute to hooks --- .../webui/src/client/lib/EditAttribute.tsx | 991 ++++++++---------- packages/webui/src/client/lib/ModalDialog.tsx | 6 +- .../src/client/ui/Settings/Migration.tsx | 4 +- 3 files changed, 438 insertions(+), 563 deletions(-) diff --git a/packages/webui/src/client/lib/EditAttribute.tsx b/packages/webui/src/client/lib/EditAttribute.tsx index 7138d7645b..0fb0b5c1b6 100644 --- a/packages/webui/src/client/lib/EditAttribute.tsx +++ b/packages/webui/src/client/lib/EditAttribute.tsx @@ -1,7 +1,6 @@ import * as React from 'react' import * as _ from 'underscore' -import { withTracker } from './ReactMeteorData/react-meteor-data' - +import { useTracker } from './ReactMeteorData/react-meteor-data' import { MultiSelect, MultiSelectEvent, MultiSelectOptions } from './multiSelect' import ClassNames from 'classnames' import { ColorPickerEvent, ColorPicker } from './colorPicker' @@ -11,11 +10,12 @@ import { MongoCollection } from '../collections/lib' import { CheckboxControl } from './Components/Checkbox' import { TextInputControl } from './Components/TextInput' import { IntInputControl } from './Components/IntInput' -import { DropdownInputControl, DropdownInputOption, getDropdownInputOptions } from './Components/DropdownInput' +import { DropdownInputControl, getDropdownInputOptions } from './Components/DropdownInput' import { FloatInputControl } from './Components/FloatInput' import { joinLines, MultiLineTextInputControl, splitValueIntoLines } from './Components/MultiLineTextInput' import { JsonTextInputControl, tryParseJson } from './Components/JsonTextInput' import { ToggleSwitchControl } from './Components/ToggleSwitch' +import { useCallback, useMemo, useState } from 'react' interface IEditAttribute extends IEditAttributeBaseProps { type: EditAttributeType @@ -70,18 +70,17 @@ export class EditAttribute extends React.Component { } } -interface IEditAttributeBaseProps { +export interface IEditAttributeBaseProps { updateOnKey?: boolean attribute?: string collection?: MongoCollection - myObject?: any obj?: any options?: any optionsAreNumbers?: boolean className?: string modifiedClassName?: string invalidClassName?: string - updateFunction?: (edit: EditAttributeBase, newValue: any) => void + updateFunction?: (editProps: IEditAttributeBaseProps, newValue: any) => void overrideDisplayValue?: any label?: string mutateDisplayValue?: (v: any) => any @@ -91,601 +90,477 @@ interface IEditAttributeBaseProps { /** Defaults to string */ arrayType?: 'boolean' | 'int' | 'float' | 'string' } -interface IEditAttributeBaseState { - value: any - valueError: boolean - editing: boolean -} -export class EditAttributeBase extends React.Component { - constructor(props: IEditAttributeBaseProps) { - super(props) - - this.state = { - value: this.getAttribute(), - valueError: false, - editing: false, - } - this.handleEdit = this.handleEdit.bind(this) - this.handleUpdate = this.handleUpdate.bind(this) - this.handleDiscard = this.handleDiscard.bind(this) - } - /** Update the temporary value of this field, optionally saving a value */ - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - protected handleEdit(inputValue: any, storeValue?: any): void { - this.setState({ - value: inputValue, - editing: true, - }) - if (this.props.updateOnKey) { - this.updateValue(storeValue ?? inputValue) - } - } - /** Update and save the value of this field */ - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - protected handleUpdate(inputValue: any, storeValue?: any): void { - this.handleUpdateButDontSave(inputValue) - this.updateValue(storeValue ?? inputValue) - } - /** Update the temporary value of this field, and save it */ - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - protected handleUpdateEditing(newValue: any): void { - this.handleUpdateButDontSave(newValue, true) - this.updateValue(newValue) - } - /** Update the temporary value of this field, marking whether is being edited */ - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - protected handleUpdateButDontSave(newValue: any, editing = false): void { - this.setState({ - value: newValue, - editing, - }) - } - /** Discard the temporary value of this field */ - protected handleDiscard(): void { - this.setState({ - value: this.getAttribute(), - editing: false, - }) - } - private deepAttribute(obj0: any, attr0: string | undefined): any { - // Returns a value deep inside an object - // Example: deepAttribute(company,"ceo.address.street"); +function EditAttributeText(props: IEditAttributeBaseProps) { + const stateHelper = useEditAttributeStateHelper(props) + + return ( + + ) +} +function EditAttributeMultilineText(props: IEditAttributeBaseProps) { + const stateHelper = useEditAttributeStateHelper(props) + + const handleChange = useCallback( + (value: string[]) => { + stateHelper.handleUpdate(joinLines(value)) // as single string + }, + [stateHelper.handleUpdate] + ) + + return ( + + ) +} +function EditAttributeInt(props: IEditAttributeBaseProps) { + const stateHelper = useEditAttributeStateHelper(props) + + return ( + + ) +} +function EditAttributeFloat(props: IEditAttributeBaseProps) { + const stateHelper = useEditAttributeStateHelper(props) + + return ( + + ) +} +function EditAttributeCheckbox(props: IEditAttributeBaseProps) { + const stateHelper = useEditAttributeStateHelper(props) + + return ( + + ) +} +function EditAttributeToggle(props: IEditAttributeBaseProps) { + const stateHelper = useEditAttributeStateHelper(props) + + return ( + + ) +} +function EditAttributeDropdown(props: IEditAttributeBaseProps) { + const stateHelper = useEditAttributeStateHelper(props) + + const options = useMemo(() => getDropdownInputOptions(props.options), [props.options]) + + const handleChange = useCallback( + (value: string) => { + stateHelper.handleUpdate(props.optionsAreNumbers ? parseInt(value, 10) : value) + }, + [stateHelper.handleUpdate, props.optionsAreNumbers] + ) + + return ( + + ) +} +function EditAttributeDropdownText(props: IEditAttributeBaseProps) { + const stateHelper = useEditAttributeStateHelper(props) + + const options = useMemo(() => getDropdownInputOptions(props.options), [props.options]) + + return ( + + ) +} - const f = (obj: any, attr: string): any => { - if (obj) { - const attributes = attr.split('.') +interface EditAttributeMultiSelectOptionsResult { + options: MultiSelectOptions + currentOptionMissing: boolean +} - if (attributes.length > 1) { - const outerAttr = attributes.shift() as string - const innerAttrs = attributes.join('.') +function EditAttributeMultiSelect(props: IEditAttributeBaseProps) { + const stateHelper = useEditAttributeStateHelper(props) + + const currentValue = stateHelper.getAttributeValue() + const options = getMultiselectOptions(props.options, currentValue, true) + + const handleChange = useCallback( + (event: MultiSelectEvent) => stateHelper.handleUpdate(event.selectedValues), + [stateHelper.handleUpdate] + ) + + return ( + + ) +} - return f(obj[outerAttr], innerAttrs) - } else { - return obj[attributes[0]] - } +function getMultiselectOptions( + rawOptions: any, + currentValue: any, + addOptionsForCurrentValue?: boolean +): EditAttributeMultiSelectOptionsResult { + const options: MultiSelectOptions = {} + + if (Array.isArray(rawOptions)) { + // is it an enum? + for (const val of rawOptions) { + if (typeof val === 'object') { + options[val.value] = { value: val.name } } else { - return obj + options[val] = { value: val } } } - return f(obj0, attr0 || '') - } - protected getAttribute(): any { - let v = null - if (this.props.overrideDisplayValue !== undefined) { - v = this.props.overrideDisplayValue - } else { - v = this.deepAttribute(this.props.myObject, this.props.attribute) - } - return this.props.mutateDisplayValue ? this.props.mutateDisplayValue(v) : v - } - - protected getEditAttribute(): any { - return this.state.editing ? this.state.value : this.getAttribute() - } - private updateValue(newValue: any) { - if (this.props.mutateUpdateValue) { - try { - newValue = this.props.mutateUpdateValue(newValue) - this.setState({ - valueError: false, - }) - } catch (e) { - this.setState({ - valueError: true, - editing: true, - }) - return + } else if (typeof rawOptions === 'object') { + // Is options an enum? + const keys = Object.keys(rawOptions) + const first = rawOptions[keys[0]] + if (rawOptions[first] + '' === keys[0] + '') { + // is an enum, only pick + for (const key in rawOptions) { + if (!_.isNaN(parseInt(key, 10))) { + // key is a number (the key) + const enumValue = rawOptions[key] + const enumKey = rawOptions[enumValue] + options[enumKey] = { value: enumValue } + } } - } - - if (this.props.updateFunction && typeof this.props.updateFunction === 'function') { - this.props.updateFunction(this, newValue) } else { - if (this.props.collection && this.props.attribute) { - if (newValue === undefined) { - const m: Record = {} - m[this.props.attribute] = 1 - this.props.collection.update(this.props.obj._id, { $unset: m }) + for (const key in rawOptions) { + const val = rawOptions[key] + if (Array.isArray(val)) { + options[key] = { value: val } } else { - const m: Record = {} - m[this.props.attribute] = newValue - this.props.collection.update(this.props.obj._id, { $set: m }) + options[val] = { value: key + ': ' + val } } } } } -} -function wrapEditAttribute(newClass: any) { - return withTracker((props: IEditAttributeBaseProps) => { - // These properties will be exposed under this.props - // Note that these properties are reactively recalculated - return { - myObject: props.collection ? props.collection.findOne(props.obj._id) : props.obj || {}, - } - })(newClass) -} -const EditAttributeText = wrapEditAttribute( - class EditAttributeText extends EditAttributeBase { - constructor(props: any) { - super(props) + const missingOptions = Array.isArray(currentValue) ? currentValue.filter((v) => !(v in options)) : [] - this.handleChange = this.handleChange.bind(this) - } - private handleChange(value: string) { - this.handleUpdate(value) - } - render(): JSX.Element { - return ( - - ) - } + if (addOptionsForCurrentValue) { + missingOptions.forEach((option) => { + options[option] = { value: `${option}`, className: 'option-missing' } + }) } -) -const EditAttributeMultilineText = wrapEditAttribute( - class EditAttributeMultilineText extends EditAttributeBase { - constructor(props: any) { - super(props) - this.handleChange = this.handleChange.bind(this) - } - private handleChange(value: string[]) { - this.handleUpdate(joinLines(value)) // as single string - } - render(): JSX.Element { - return ( - - ) - } - } -) -const EditAttributeInt = wrapEditAttribute( - class EditAttributeInt extends EditAttributeBase { - constructor(props: any) { - super(props) + return { options, currentOptionMissing: !!missingOptions.length } +} - this.handleChange = this.handleChange.bind(this) - } - private handleChange(value: number) { - this.handleUpdate(value) - } - render(): JSX.Element { - return ( - - ) - } - } -) -const EditAttributeFloat = wrapEditAttribute( - class EditAttributeFloat extends EditAttributeBase { - constructor(props: any) { - super(props) +function EditAttributeJson(props: IEditAttributeBaseProps) { + const stateHelper = useEditAttributeStateHelper(props) + + const handleChange = useCallback( + (value: object) => { + const storeValue = props.storeJsonAsObject ? value : JSON.stringify(value, undefined, 2) + stateHelper.handleUpdate(storeValue) + }, + [stateHelper.handleUpdate, props.storeJsonAsObject] + ) + + const value = props.storeJsonAsObject + ? stateHelper.getAttributeValue() + : tryParseJson(stateHelper.getAttributeValue())?.parsed + + return ( + + ) +} +function EditAttributeArray(props: IEditAttributeBaseProps) { + const stateHelper = useEditAttributeStateHelper(props) - this.handleChange = this.handleChange.bind(this) - } - private handleChange(value: number) { - this.handleUpdate(value) - } - render(): JSX.Element { - return ( - - ) - } - } -) -const EditAttributeCheckbox = wrapEditAttribute( - class EditAttributeCheckbox extends EditAttributeBase { - constructor(props: any) { - super(props) + const [editingValue, setEditingValue] = useState(null) - this.handleChange = this.handleChange.bind(this) - } - private handleChange(value: boolean) { - this.handleUpdate(value) - } - render(): JSX.Element { - const classNames = _.compact([ - this.props.className, - this.state.editing ? this.props.modifiedClassName : undefined, - ]).join(' ') - - return ( - - ) - } - } -) -const EditAttributeToggle = wrapEditAttribute( - class EditAttributeToggle extends EditAttributeBase { - constructor(props: any) { - super(props) - } - private isChecked() { - return !!this.getEditAttribute() - } + const handleChange = useCallback( + (event: React.ChangeEvent) => { + const v = event.target.value - render(): JSX.Element { - return ( - - ) - } - } -) + setEditingValue(v) -const EditAttributeDropdown = wrapEditAttribute( - class EditAttributeDropdown extends EditAttributeBase { - constructor(props: any) { - super(props) + const arrayObj = stringIsArray(v, props.arrayType) + if (arrayObj) { + if (props.updateOnKey) { + stateHelper.handleUpdate(arrayObj.parsed) + } + stateHelper.setValueError(false) + } + }, + [setEditingValue, props.arrayType, props.updateOnKey, stateHelper.handleUpdate, stateHelper.setValueError] + ) + const handleBlur = useCallback( + (event: React.FocusEvent) => { + const v = event.target.value - this.handleChange = this.handleChange.bind(this) - } - handleChange(value: string) { - this.handleUpdate(this.props.optionsAreNumbers ? parseInt(value, 10) : value) - } - render(): JSX.Element { - const options = getDropdownInputOptions(this.props.options) - - return ( - - ) - } + setEditingValue(v) + + const arrayObj = stringIsArray(v, props.arrayType) + if (arrayObj) { + stateHelper.handleUpdate(arrayObj.parsed) + stateHelper.setValueError(false) + } else { + stateHelper.setValueError(true) + } + }, + [setEditingValue, props.arrayType, stateHelper.handleUpdate, stateHelper.setValueError] + ) + const handleEscape = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + setEditingValue(null) + } + }, + [setEditingValue] + ) + + let currentValueString = stateHelper.getAttributeValue() + if (Array.isArray(currentValueString)) { + currentValueString = currentValueString.join(', ') + } else { + currentValueString = '' } -) -const EditAttributeDropdownText = wrapEditAttribute( - class EditAttributeDropdownText extends EditAttributeBase { - private getOptions(): DropdownInputOption[] { - return getDropdownInputOptions(this.props.options) - } - render(): JSX.Element { - return ( - - ) + + return ( + + ) +} +function stringIsArray(strOrg: string, arrayType: IEditAttributeBaseProps['arrayType']): { parsed: any[] } | false { + if (!(strOrg + '').trim().length) return { parsed: [] } + + const values: any[] = [] + const strs = (strOrg + '').split(',') + + for (const str of strs) { + // Check that the values in the array are of the right type: + + if (arrayType === 'boolean') { + const parsed = JSON.parse(str) + if (typeof parsed !== 'boolean') return false // type check failed + values.push(parsed) + } else if (arrayType === 'int') { + const parsed = parseInt(str, 10) + + if (Number.isNaN(parsed)) return false // type check failed + values.push(parsed) + } else if (arrayType === 'float') { + const parsed = parseFloat(str) + if (Number.isNaN(parsed)) return false // type check failed + values.push(parsed) + } else { + // else this.props.arrayType is 'string' + const parsed = str + '' + if (typeof parsed !== 'string') return false // type check failed + values.push(parsed.trim()) } } -) - -interface EditAttributeMultiSelectOptionsResult { - options: MultiSelectOptions - currentOptionMissing: boolean + return { parsed: values } +} +function EditAttributeColorPicker(props: IEditAttributeBaseProps) { + const stateHelper = useEditAttributeStateHelper(props) + + const handleChange = useCallback( + (event: ColorPickerEvent) => stateHelper.handleUpdate(event.selectedValue), + [stateHelper.handleUpdate] + ) + + return ( + + ) +} +function EditAttributeIconPicker(props: IEditAttributeBaseProps) { + const stateHelper = useEditAttributeStateHelper(props) + + const handleChange = useCallback( + (event: IconPickerEvent) => stateHelper.handleUpdate(event.selectedValue), + [stateHelper.handleUpdate] + ) + + return ( + + ) } -const EditAttributeMultiSelect = wrapEditAttribute( - class EditAttributeMultiSelect extends EditAttributeBase { - constructor(props: any) { - super(props) - - this.handleChange = this.handleChange.bind(this) - } - handleChange(event: MultiSelectEvent) { - this.handleUpdate(event.selectedValues) - } - getOptions(addOptionsForCurrentValue?: boolean): EditAttributeMultiSelectOptionsResult { - const options: MultiSelectOptions = {} - - if (Array.isArray(this.props.options)) { - // is it an enum? - for (const val of this.props.options) { - if (typeof val === 'object') { - options[val.value] = { value: val.name } - } else { - options[val] = { value: val } - } - } - } else if (typeof this.props.options === 'object') { - // Is options an enum? - const keys = Object.keys(this.props.options) - const first = this.props.options[keys[0]] - if (this.props.options[first] + '' === keys[0] + '') { - // is an enum, only pick - for (const key in this.props.options) { - if (!_.isNaN(parseInt(key, 10))) { - // key is a number (the key) - const enumValue = this.props.options[key] - const enumKey = this.props.options[enumValue] - options[enumKey] = { value: enumValue } - } - } - } else { - for (const key in this.props.options) { - const val = this.props.options[key] - if (Array.isArray(val)) { - options[key] = { value: val } - } else { - options[val] = { value: key + ': ' + val } - } - } - } - } +interface EditAttributeStateHelper { + props: Readonly + myObject: any - const currentValue = this.getAttribute() - const missingOptions = Array.isArray(currentValue) ? currentValue.filter((v) => !(v in options)) : [] + valueError: boolean + setValueError: (value: boolean) => void - if (addOptionsForCurrentValue) { - missingOptions.forEach((option) => { - options[option] = { value: `${option}`, className: 'option-missing' } - }) - } + getAttributeValue: () => any + handleUpdate: (inputValue: any, storeValue?: any) => void +} - return { options, currentOptionMissing: !!missingOptions.length } - } - render(): JSX.Element { - const options = this.getOptions(true) - return ( - - ) - } - } -) +function useEditAttributeStateHelper(props: IEditAttributeBaseProps): EditAttributeStateHelper { + const [valueError, setValueError] = useState(false) -const EditAttributeJson = wrapEditAttribute( - class EditAttributeJson extends EditAttributeBase { - constructor(props: any) { - super(props) + const myObject = useTracker( + () => (props.collection ? props.collection.findOne(props.obj._id) : props.obj || {}), + [props.collection, props.obj] + ) - this.handleChange = this.handleChange.bind(this) - } - private handleChange(value: object) { - const storeValue = this.props.storeJsonAsObject ? value : JSON.stringify(value, undefined, 2) - this.handleUpdate(storeValue) - } - render(): JSX.Element { - const value = this.props.storeJsonAsObject ? this.getAttribute() : tryParseJson(this.getAttribute())?.parsed - - return ( - - ) - } - } -) -const EditAttributeArray = wrapEditAttribute( - class EditAttributeArray extends EditAttributeBase { - constructor(props: any) { - super(props) - - this.handleChange = this.handleChange.bind(this) - this.handleBlur = this.handleBlur.bind(this) - this.handleEscape = this.handleEscape.bind(this) + const getAttributeValue = useCallback((): any => { + let v = null + if (props.overrideDisplayValue !== undefined) { + v = props.overrideDisplayValue + } else { + v = deepAttribute(myObject, props.attribute) } - isArray(strOrg: string): { parsed: any[] } | false { - if (!(strOrg + '').trim().length) return { parsed: [] } - - const values: any[] = [] - const strs = (strOrg + '').split(',') - - for (const str of strs) { - // Check that the values in the array are of the right type: - - if (this.props.arrayType === 'boolean') { - const parsed = JSON.parse(str) - if (typeof parsed !== 'boolean') return false // type check failed - values.push(parsed) - } else if (this.props.arrayType === 'int') { - const parsed = parseInt(str, 10) - - if (Number.isNaN(parsed)) return false // type check failed - values.push(parsed) - } else if (this.props.arrayType === 'float') { - const parsed = parseFloat(str) - if (Number.isNaN(parsed)) return false // type check failed - values.push(parsed) - } else { - // else this.props.arrayType is 'string' - const parsed = str + '' - if (typeof parsed !== 'string') return false // type check failed - values.push(parsed.trim()) + return props.mutateDisplayValue ? props.mutateDisplayValue(v) : v + }, [props.overrideDisplayValue, props.mutateDisplayValue, deepAttribute, myObject, props.attribute]) + + /** Update and save the value of this field */ + const handleUpdate = useCallback( + (newValue: any) => { + if (props.mutateUpdateValue) { + try { + newValue = props.mutateUpdateValue(newValue) + setValueError(false) + } catch (e) { + setValueError(true) + + return } } - return { parsed: values } - } - handleChange(event: React.ChangeEvent) { - const v = event.target.value - const arrayObj = this.isArray(v) - if (arrayObj) { - this.handleEdit(v, arrayObj.parsed) - this.setState({ - valueError: false, - }) + if (props.updateFunction && typeof props.updateFunction === 'function') { + props.updateFunction(props, newValue) } else { - this.handleUpdateButDontSave(v, true) + if (props.collection && props.attribute) { + if (newValue === undefined) { + const m: Record = {} + m[props.attribute] = 1 + props.collection.update(props.obj._id, { $unset: m }) + } else { + const m: Record = {} + m[props.attribute] = newValue + props.collection.update(props.obj._id, { $set: m }) + } + } } - } - handleBlur(event: React.FocusEvent) { - const v = event.target.value + }, + [props, props.mutateUpdateValue, props.updateFunction, props.collection, props.attribute, props.obj?._id] + ) - const arrayObj = this.isArray(v) - if (arrayObj) { - this.handleUpdate(v, arrayObj.parsed) - this.setState({ - valueError: false, - }) - } else { - this.handleUpdateButDontSave(v, true) - this.setState({ - valueError: true, - }) - } - } - handleEscape(e: React.KeyboardEvent) { - if (e.key === 'Escape') { - this.handleDiscard() - } - } - getAttribute() { - const value = super.getAttribute() - if (Array.isArray(value)) { - return value.join(', ') - } else { - return '' - } - } - render(): JSX.Element { - return ( - - ) - } - } -) + return { props, myObject, valueError, setValueError, getAttributeValue, handleUpdate } +} -const EditAttributeColorPicker = wrapEditAttribute( - class EditAttributeColorPicker extends EditAttributeBase { - constructor(props: any) { - super(props) +function deepAttribute(obj0: any, attr0: string | undefined): any { + // Returns a value deep inside an object + // Example: deepAttribute(company,"ceo.address.street"); - this.handleChange = this.handleChange.bind(this) - } - handleChange(event: ColorPickerEvent) { - this.handleUpdate(event.selectedValue) - } - render(): JSX.Element { - return ( - - ) - } - } -) -const EditAttributeIconPicker = wrapEditAttribute( - class extends EditAttributeBase { - constructor(props: any) { - super(props) + const f = (obj: any, attr: string): any => { + if (obj) { + const attributes = attr.split('.') - this.handleChange = this.handleChange.bind(this) - } - handleChange(event: IconPickerEvent) { - this.handleUpdate(event.selectedValue) - } - render(): JSX.Element { - return ( - - ) + if (attributes.length > 1) { + const outerAttr = attributes.shift() as string + const innerAttrs = attributes.join('.') + + return f(obj[outerAttr], innerAttrs) + } else { + return obj[attributes[0]] + } + } else { + return obj } } -) + return f(obj0, attr0 || '') +} diff --git a/packages/webui/src/client/lib/ModalDialog.tsx b/packages/webui/src/client/lib/ModalDialog.tsx index 0243afef23..089625e53f 100644 --- a/packages/webui/src/client/lib/ModalDialog.tsx +++ b/packages/webui/src/client/lib/ModalDialog.tsx @@ -12,7 +12,7 @@ import { logger } from './logging' import * as _ from 'underscore' import { withTranslation } from 'react-i18next' import { Translated } from './ReactMeteorData/ReactMeteorData' -import { EditAttribute, EditAttributeType, EditAttributeBase } from './EditAttribute' +import { EditAttribute, EditAttributeType, IEditAttributeBaseProps } from './EditAttribute' import { Settings } from '../lib/Settings' interface IModalDialogAttributes { @@ -88,8 +88,8 @@ export function ModalDialog({ callback(e, inputResult.current) } - function updateInput(edit: EditAttributeBase, newValue: any) { - inputResult.current[edit.props.attribute || ''] = newValue + function updateInput(editProps: IEditAttributeBaseProps, newValue: any) { + inputResult.current[editProps.attribute || ''] = newValue } function emulateClick(e: React.KeyboardEvent) { diff --git a/packages/webui/src/client/ui/Settings/Migration.tsx b/packages/webui/src/client/ui/Settings/Migration.tsx index 20f77b6a65..234371ebed 100644 --- a/packages/webui/src/client/ui/Settings/Migration.tsx +++ b/packages/webui/src/client/ui/Settings/Migration.tsx @@ -12,7 +12,7 @@ import { } from '@sofie-automation/meteor-lib/dist/api/migration' import { MigrationStepInput, MigrationStepInputResult } from '@sofie-automation/blueprints-integration' import * as _ from 'underscore' -import { EditAttribute, EditAttributeBase } from '../../lib/EditAttribute' +import { EditAttribute } from '../../lib/EditAttribute' import { MeteorCall } from '../../lib/meteorApi' import { checkForOldDataAndCleanUp } from './SystemManagement' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' @@ -232,7 +232,7 @@ export const MigrationView = translateWithTracker className="input-full mtxs" options={manualInput.dropdownOptions} overrideDisplayValue={value} - updateFunction={(_edit: EditAttributeBase, newValue: any) => { + updateFunction={(_edit, newValue: any) => { if (manualInput.attribute) { const inputValues = this.state.inputValues if (!inputValues[stepId]) inputValues[stepId] = {} From 8c5c93355ce6759a47e9626b593f480e7df80309 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 18 Dec 2024 14:22:53 +0000 Subject: [PATCH 3/3] chore: remove unused EditAttribute 'array' type --- .../webui/src/client/lib/EditAttribute.tsx | 108 ------------------ 1 file changed, 108 deletions(-) diff --git a/packages/webui/src/client/lib/EditAttribute.tsx b/packages/webui/src/client/lib/EditAttribute.tsx index 0fb0b5c1b6..96ae20cea1 100644 --- a/packages/webui/src/client/lib/EditAttribute.tsx +++ b/packages/webui/src/client/lib/EditAttribute.tsx @@ -33,7 +33,6 @@ export type EditAttributeType = | 'json' | 'colorpicker' | 'iconpicker' - | 'array' export class EditAttribute extends React.Component { render(): JSX.Element { if (this.props.type === 'text') { @@ -60,8 +59,6 @@ export class EditAttribute extends React.Component { return } else if (this.props.type === 'iconpicker') { return - } else if (this.props.type === 'array') { - return } else { assertNever(this.props.type) } @@ -335,111 +332,6 @@ function EditAttributeJson(props: IEditAttributeBaseProps) { /> ) } -function EditAttributeArray(props: IEditAttributeBaseProps) { - const stateHelper = useEditAttributeStateHelper(props) - - const [editingValue, setEditingValue] = useState(null) - - const handleChange = useCallback( - (event: React.ChangeEvent) => { - const v = event.target.value - - setEditingValue(v) - - const arrayObj = stringIsArray(v, props.arrayType) - if (arrayObj) { - if (props.updateOnKey) { - stateHelper.handleUpdate(arrayObj.parsed) - } - stateHelper.setValueError(false) - } - }, - [setEditingValue, props.arrayType, props.updateOnKey, stateHelper.handleUpdate, stateHelper.setValueError] - ) - const handleBlur = useCallback( - (event: React.FocusEvent) => { - const v = event.target.value - - setEditingValue(v) - - const arrayObj = stringIsArray(v, props.arrayType) - if (arrayObj) { - stateHelper.handleUpdate(arrayObj.parsed) - stateHelper.setValueError(false) - } else { - stateHelper.setValueError(true) - } - }, - [setEditingValue, props.arrayType, stateHelper.handleUpdate, stateHelper.setValueError] - ) - const handleEscape = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === 'Escape') { - setEditingValue(null) - } - }, - [setEditingValue] - ) - - let currentValueString = stateHelper.getAttributeValue() - if (Array.isArray(currentValueString)) { - currentValueString = currentValueString.join(', ') - } else { - currentValueString = '' - } - - return ( - - ) -} -function stringIsArray(strOrg: string, arrayType: IEditAttributeBaseProps['arrayType']): { parsed: any[] } | false { - if (!(strOrg + '').trim().length) return { parsed: [] } - - const values: any[] = [] - const strs = (strOrg + '').split(',') - - for (const str of strs) { - // Check that the values in the array are of the right type: - - if (arrayType === 'boolean') { - const parsed = JSON.parse(str) - if (typeof parsed !== 'boolean') return false // type check failed - values.push(parsed) - } else if (arrayType === 'int') { - const parsed = parseInt(str, 10) - - if (Number.isNaN(parsed)) return false // type check failed - values.push(parsed) - } else if (arrayType === 'float') { - const parsed = parseFloat(str) - if (Number.isNaN(parsed)) return false // type check failed - values.push(parsed) - } else { - // else this.props.arrayType is 'string' - const parsed = str + '' - if (typeof parsed !== 'string') return false // type check failed - values.push(parsed.trim()) - } - } - return { parsed: values } -} function EditAttributeColorPicker(props: IEditAttributeBaseProps) { const stateHelper = useEditAttributeStateHelper(props)