From 55d1aca9b64f5ba0d9899150a2aefdd5c5e2b04b Mon Sep 17 00:00:00 2001 From: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Date: Sat, 13 Apr 2024 10:06:46 +0200 Subject: [PATCH] V3 (#657) * refactor: cleanup routes * style: smaller base font * chore: upgrade dependencies * chore: lock node version to electron * refactor: pass HTTP to integration controller (#652) * refactor: deprecate onair control * refactor: remove playback router * Several project files user folder (#617) * chore: automated screenshots (#667) * feat: app settings (#658) * refactor: remove deprecated event data (#674) * Studio clock (#663) --------- Co-authored-by: Carlos Valente * Feat: reorder events with alt+ctrl + arrow up/down (#645) * Warning and danger per event (#677) --------- Co-authored-by: Fabian Posenau * refactor: stabilise actionHandler (#683) Co-authored-by: Fabian Posenau * improvement: hide seconds (#675) * wip: overview (#688) * fix: focus cursor (#695) * refactor: update lower third (#665) * Refactor/time formatting (#696) --------- Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Co-authored-by: Carlos Valente * feat: multiple selection (#703) --------- Co-authored-by: asharonbaltazar Co-authored-by: Alex * fix: test - go to `Edit mode` befor tying to click `Event options` button (#708) * refactor: runtime service (#715) * fix: issue with loosing cursor position on message (#719) * remove info panel (#721) * Event editor continue (#722) * update API - part (#709) --------- Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Co-authored-by: Carlos Valente * refactor: update timers (#729) * feat: many timers (#706) --------- Co-authored-by: arc-alex * refactor: excel cleanup (#734) * refactor: allow import of blocks and skip import (#735) * Project manager (#697) * refactor: UI for linking events (#763) * upgraded pipeline actions (#777) * Over under (#771) * custom fields (#744) --------- Co-authored-by: Carlos Valente * Sheets settings (#774) --------- Co-authored-by: arc-alex * style: tweaks to lower thirds (#785) * refactor: delays account for gaps (#784) * refactor: partial state updates (#780) * feat: generate crash report (#787) * Sheet use limited input device auth flow (#782) --------- Co-authored-by: cv <34649812+cpvalente@users.noreply.github.com> Co-authored-by: Carlos Valente * Custom fields views (#789) * refactor: deprecate presenter and subtitle (#795) * refactor: organise API around resources (#798) --------- Co-authored-by: Bianca Procopio * Time to end (#804) * Skip fixes (#805) * fix: onair derives from playback * Param nav (#822) --------- Co-authored-by: Alex Christoffer Rasmussen * refactor: download files from interface (#831) * Quick options (#814) * End pause (#832) * chore: bump node version in docker (#834) * refactor: follow in run mode (#840) * fix: uncaught error in http integration (#837) * Apply project (#843) Co-authored-by: Matteo Gheza Co-authored-by: Ary Co-authored-by: Alex Christoffer Rasmussen Co-authored-by: Fabian Posenau <19673098+kellhogs@users.noreply.github.com> Co-authored-by: Fabian Posenau Co-authored-by: Alex Rohleder Co-authored-by: asharonbaltazar Co-authored-by: Bianca Procopio Co-authored-by: Fabian Posenau --- .eslintrc | 35 +- .github/workflows/build_v2.yml | 6 +- .github/workflows/build_v3.yml | 109 + .github/workflows/test_v2.yml | 10 +- .github/workflows/test_v3.yml | 107 + .gitignore | 4 + .nvmrc | 2 +- DEVELOPMENT.md | 11 +- Dockerfile | 2 +- README.md | 28 +- apps/client/package.json | 49 +- apps/client/src/App.tsx | 3 +- apps/client/src/AppRouter.tsx | 40 +- .../assets/fonts/DSEG7ClassicMini-Regular.ttf | Bin 0 -> 23116 bytes .../src/assets/fonts/digital-7.monoitalic.ttf | Bin 22984 -> 0 bytes .../api/{apiConstants.ts => constants.ts} | 22 +- apps/client/src/common/api/customFields.ts | 38 + apps/client/src/common/api/db.ts | 168 + apps/client/src/common/api/eventsApi.ts | 90 - apps/client/src/common/api/excel.ts | 42 + apps/client/src/common/api/external.ts | 19 + apps/client/src/common/api/http.ts | 21 + apps/client/src/common/api/ontimeApi.ts | 320 -- apps/client/src/common/api/osc.ts | 21 + apps/client/src/common/api/project.ts | 21 + apps/client/src/common/api/projectDataApi.ts | 21 - apps/client/src/common/api/rundown.ts | 86 + apps/client/src/common/api/settings.ts | 21 + apps/client/src/common/api/sheets.ts | 75 + apps/client/src/common/api/urlPresets.ts | 21 + .../src/common/api/{apiUtils.ts => utils.ts} | 46 +- apps/client/src/common/api/viewSettings.ts | 21 + .../components/app-link/AppLink.module.scss | 12 + .../common/components/app-link/AppLink.tsx | 26 + .../common/components/buttons/QuitIconBtn.tsx | 12 +- .../components/buttons/TooltipActionBtn.tsx | 2 +- .../collapse-bar/CollapseBar.module.scss | 24 - .../components/collapse-bar/CollapseBar.tsx | 20 - .../context-menu/ContextMenu.module.scss | 1 - .../components/context-menu/ContextMenu.tsx | 40 +- .../context-menu/ContextMenuOption.tsx | 12 + .../common/components/copy-tag/CopyTag.tsx | 14 +- .../DelayIndicator.module.scss | 2 - .../error-boundary/ErrorBoundary.jsx | 6 +- .../error-boundary/ErrorBoundary.module.scss | 2 - .../external-link/ExternalLink.module.scss} | 6 +- .../external-link/ExternalLink.tsx} | 10 +- .../input/auto-text-area/AutoTextArea.tsx | 4 +- .../components/input/colour-input/Swatch.tsx | 11 +- .../colour-input/SwatchSelect.module.scss | 21 +- .../input/colour-input/SwatchSelect.tsx | 6 +- .../input/delay-input/DelayInput.module.scss | 2 - .../input/delay-input/DelayInput.tsx | 1 + .../popover-picker/PopoverPicker.module.scss | 3 - .../components/input/text-input/TextInput.tsx | 2 +- .../input/time-input/TimeInput.module.scss | 35 +- .../components/input/time-input/TimeInput.tsx | 143 +- .../TimeInputWithButton.module.scss | 12 + .../input/time-input/TimeInputWithButton.tsx | 37 + .../loader-overlay/LoaderOverlay.module.scss} | 9 +- .../loader-overlay/LoaderOverlay.tsx | 9 + .../MultiPartProgressBar.tsx | 16 +- .../navigation-menu/FloatingNavigation.tsx | 60 + .../NavigationMenu.module.scss | 37 +- .../navigation-menu/NavigationMenu.tsx | 130 +- .../ProductionNavigationMenu.tsx | 16 + .../navigation-menu/ViewNavigationMenu.tsx | 27 + .../RenameClientModal.module.scss | 5 - .../rename-client-modal/RenameClientModal.tsx | 10 +- .../components/progress-bar/ProgressBar.scss | 4 +- .../components/protect-route/PinPage.tsx | 17 +- .../protect-route/ProtectRoute.module.scss | 34 +- .../common/components/schedule/Schedule.scss | 7 +- .../components/schedule/ScheduleContext.tsx | 2 +- .../components/schedule/ScheduleItem.tsx | 7 +- .../common/components/state/Empty.module.scss | 4 +- .../src/common/components/state/Empty.tsx | 8 +- .../components/tag}/Tag.module.scss | 6 +- .../preview => common/components/tag}/Tag.tsx | 0 .../components/timer-display/TimerDisplay.tsx | 33 - .../components/title-card/TitleCard.scss | 54 +- .../components/title-card/TitleCard.tsx | 16 +- .../view-params-editor/ParamInput.tsx | 2 +- .../ViewParamsEditor.module.scss | 31 +- .../view-params-editor/ViewParamsEditor.tsx | 70 +- .../view-params-editor/constants.ts | 479 ++- apps/client/src/common/context/AppContext.tsx | 5 - .../src/common/hooks-query/useAliases.ts | 19 - .../src/common/hooks-query/useCustomFields.ts | 22 + .../src/common/hooks-query/useHttpSettings.ts | 15 +- apps/client/src/common/hooks-query/useInfo.ts | 8 +- .../src/common/hooks-query/useOscSettings.ts | 26 +- .../src/common/hooks-query/useProjectData.ts | 8 +- .../src/common/hooks-query/useProjectList.ts | 24 + .../src/common/hooks-query/useRundown.ts | 44 +- .../src/common/hooks-query/useSettings.ts | 19 +- .../src/common/hooks-query/useUrlPresets.ts | 19 + .../src/common/hooks-query/useUserFields.ts | 20 - .../src/common/hooks-query/useViewSettings.ts | 8 +- .../client/src/common/hooks/useEventAction.ts | 303 +- apps/client/src/common/hooks/useFullscreen.ts | 68 - apps/client/src/common/hooks/useMemoisedFn.ts | 38 + .../src/common/hooks/useScrollIntoView.tsx | 15 + apps/client/src/common/hooks/useSocket.ts | 151 +- .../client/src/common/hooks/useWindowTitle.ts | 11 + apps/client/src/common/models/Http.ts | 9 +- apps/client/src/common/models/Info.ts | 18 +- apps/client/src/common/models/OscSettings.ts | 23 +- .../src/common/models/TimeManager.type.ts | 29 +- apps/client/src/common/models/UserFields.ts | 14 - apps/client/src/common/models/View.types.ts | 1 + .../src/common/models/ViewSettings.type.ts | 9 +- apps/client/src/common/stores/appModeStore.ts | 61 +- .../src/common/stores/editorSettings.ts | 43 +- apps/client/src/common/stores/runtime.ts | 103 +- .../common/utils/__tests__/dateConfig.test.js | 99 +- .../utils/__tests__/eventsManager.test.ts | 24 +- .../src/common/utils/__tests__/regex.test.ts | 38 +- .../src/common/utils/__tests__/time.test.ts | 39 +- .../{aliases.test.js => urlPresets.test.js} | 34 +- apps/client/src/common/utils/aliases.ts | 77 - apps/client/src/common/utils/dateConfig.ts | 91 +- apps/client/src/common/utils/eventsManager.ts | 15 +- apps/client/src/common/utils/fileDownload.ts | 56 - apps/client/src/common/utils/keyEvent.ts | 9 + apps/client/src/common/utils/regex.ts | 9 + apps/client/src/common/utils/socket.ts | 93 +- apps/client/src/common/utils/styleUtils.ts | 5 + apps/client/src/common/utils/time.ts | 82 +- apps/client/src/common/utils/timeConstants.js | 17 - apps/client/src/common/utils/timesManager.ts | 1 - apps/client/src/common/utils/uploadUtils.ts | 47 + apps/client/src/common/utils/urlPresets.ts | 76 + apps/client/src/common/utils/viewUtils.ts | 6 - apps/client/src/externals.ts | 3 +- .../src/features/FeatureWrapper.module.scss | 4 +- .../{AliasWrapper.tsx => PresetWrapper.tsx} | 12 +- .../app-settings/AppSettings.module.scss | 11 + .../src/features/app-settings/AppSettings.tsx | 40 + .../panel-content/PanelContent.module.scss | 21 + .../panel-content/PanelContent.tsx | 24 + .../panel-list/PanelList.module.scss | 65 + .../app-settings/panel-list/PanelList.tsx | 66 + .../app-settings/panel/Panel.module.scss | 174 + .../app-settings/panel/PanelUtils.tsx | 94 + .../panel/about-panel/AboutPanel.tsx | 31 + .../panel/about-panel/CheckUpdatesButton.tsx} | 34 +- .../FeatureSettings.module.scss | 39 + .../FeatureSettingsPanel.tsx | 24 + .../feature-settings-panel/UrlPresetsForm.tsx | 207 ++ .../custom-fields/CustomFieldEntry.tsx | 71 + .../custom-fields/CustomFieldForm.tsx | 102 + .../custom-fields/CustomFields.tsx | 98 + .../general-panel/GeneralPanel.module.scss | 4 + .../panel/general-panel/GeneralPanel.tsx | 28 + .../panel/general-panel/GeneralPanelForm.tsx | 163 + .../panel/general-panel/GeneralPinInput.tsx} | 20 +- .../panel/general-panel/ViewSettingsForm.tsx | 159 + .../integrations-panel/HttpIntegrations.tsx | 189 ++ .../IntegrationsPanel.module.css | 16 + .../integrations-panel/IntegrationsPanel.tsx | 42 + .../integrations-panel/OscIntegrations.tsx | 305 ++ .../integrations-panel/integrationUtils.ts | 17 + .../interface-panel/EditorSettingsForm.tsx | 100 + .../panel/interface-panel/InterfacePanel.tsx | 27 + .../panel/log-panel/LogExport.module.scss | 3 + .../panel/log-panel/LogExport.tsx | 35 + .../app-settings/panel/log-panel/LogPanel.tsx | 17 + .../log-panel/NetworkInterfaces.module.scss | 6 + .../panel/log-panel/NetworkInterfaces.tsx | 19 + .../panel/project-panel/ManageProjects.tsx | 96 + .../panel/project-panel/ProjectCreateForm.tsx | 150 + .../panel/project-panel/ProjectData.tsx | 148 + .../panel/project-panel/ProjectForm.tsx | 70 + .../panel/project-panel/ProjectList.tsx | 67 + .../panel/project-panel/ProjectListItem.tsx | 179 + .../project-panel/ProjectPanel.module.scss | 52 + .../panel/project-panel/ProjectPanel.tsx | 23 + .../panel/shutdown-panel/ShutdownPanel.tsx | 27 + .../panel/sources-panel/GSheetInfo.tsx | 21 + .../panel/sources-panel/GSheetSetup.tsx | 211 ++ .../panel/sources-panel/ImportReview.tsx | 55 + .../sources-panel/SourcesPanel.module.scss | 40 + .../panel/sources-panel/SourcesPanel.tsx | 216 ++ .../import-map/ImportMapForm.tsx | 238 ++ .../__test__/importMapUtils.test.ts | 38 + .../import-map/importMapUtils.ts | 61 + .../preview/PreviewRundown.module.scss | 12 + .../sources-panel/preview/PreviewRundown.tsx | 122 + .../panel/sources-panel/useGoogleSheet.ts | 100 + .../panel/sources-panel/useSheetStore.ts | 76 + .../features/app-settings/settingsStore.ts | 92 + .../app-settings/useAppSettingsNavigation.tsx | 32 + .../control/message/InputRow.module.scss | 2 - .../src/features/control/message/InputRow.tsx | 21 +- .../message/MessageControl.module.scss | 17 - .../control/message/MessageControl.tsx | 70 +- .../control/playback/PlaybackControl.tsx | 2 + .../extra-timer/ExtraTimer.module.scss | 5 + .../playback/extra-timer/ExtraTimer.tsx | 74 + .../PlaybackButtons.module.scss | 2 - .../playback-buttons/PlaybackButtons.tsx | 9 +- .../playback-timer/PlaybackTimer.module.scss | 20 +- .../playback/playback-timer/PlaybackTimer.tsx | 18 +- .../playback/tap-button/TapButton.module.scss | 8 +- .../control/playback/tap-button/TapButton.tsx | 3 +- .../timer-display/TimerDisplay.module.scss} | 9 +- .../playback/timer-display/TimerDisplay.tsx | 28 + .../features/cuesheet/Cuesheet.module.scss | 15 +- .../cuesheet/CuesheetWrapper.module.scss | 18 +- .../src/features/cuesheet/CuesheetWrapper.tsx | 154 +- .../__snapshots__/utils.test.js.snap | 50 +- .../features/cuesheet/__tests__/utils.test.js | 37 +- .../cuesheet-progress/CuesheetProgress.tsx | 14 +- .../CuesheetHeader.tsx | 19 +- .../cuesheet-table-elements/DelayRow.tsx | 2 +- .../CuesheetTableHeader.module.scss | 17 +- .../CuesheetTableHeader.tsx | 35 +- .../CuesheetTableHeaderTimers.tsx | 22 +- .../CuesheetTableSettings.module.scss | 3 - .../CuesheetTableSettings.tsx | 10 +- .../src/features/cuesheet/cuesheetCols.tsx | 109 +- .../src/features/cuesheet/cuesheetUtils.ts | 134 +- apps/client/src/features/cuesheet/defaults.ts | 25 +- .../src/features/editors/Editor.module.scss | 231 +- apps/client/src/features/editors/Editor.tsx | 142 +- .../src/features/editors/EditorMixin.scss | 37 + .../event-editor/EventEditor.module.scss | 107 - .../src/features/event-editor/EventEditor.tsx | 81 - .../event-editor/EventEditorExport.tsx | 42 - .../composite/EventEditorDataLeft.tsx | 54 - .../composite/EventEditorDataRight.tsx | 33 - .../composite/EventEditorTimes.tsx | 144 - .../src/features/info/CollapsableInfo.tsx | 19 - .../client/src/features/info/Info.module.scss | 65 - apps/client/src/features/info/Info.tsx | 53 - apps/client/src/features/info/InfoExport.tsx | 24 - apps/client/src/features/info/InfoNif.tsx | 27 - apps/client/src/features/info/InfoTitles.tsx | 36 - .../features/info/info-header/InfoHeader.tsx | 17 - .../Log.module.scss} | 19 +- .../{info/InfoLogger.tsx => log/Log.tsx} | 18 +- .../src/features/menu/MenuBar.module.scss | 9 - apps/client/src/features/menu/MenuBar.tsx | 227 -- .../src/features/menu/RundownMenu.module.scss | 4 - apps/client/src/features/menu/RundownMenu.tsx | 64 - .../src/features/modals/Modal.module.scss | 185 - .../client/src/features/modals/ModalInput.tsx | 29 - .../src/features/modals/ModalSplitInput.tsx | 29 - .../src/features/modals/ModalWrapper.tsx | 32 - .../src/features/modals/OntimeModalFooter.tsx | 37 - .../modals/about-modal/AboutModal.tsx | 68 - .../export-modal/ExportModal.module.scss | 9 - .../modals/export-modal/ExportModal.tsx | 32 - .../integration-modal/IntegrationModal.tsx | 51 - .../http/HttpIntegration.tsx | 152 - .../http/HttpSubscriptionRow.tsx | 95 - .../integration-modal/integration.utils.ts | 30 - .../integration-modal/osc/OscIntegration.tsx | 139 - .../integration-modal/osc/OscSettings.tsx | 179 - .../osc/OscSubscriptionRow.tsx | 92 - .../modals/modal-loader/ModalLoader.tsx | 9 - .../client/src/features/modals/modalHelper.js | 4 - .../modals/quick-start/QuickStart.tsx | 183 - .../modals/settings-modal/AliasesForm.tsx | 178 - .../modals/settings-modal/AppSettings.tsx | 138 - .../settings-modal/CuesheetSettingsForm.tsx | 183 - .../modals/settings-modal/EditorSettings.tsx | 57 - .../settings-modal/InputMillisWithString.tsx | 35 - .../modals/settings-modal/ProjectDataForm.tsx | 139 - .../settings-modal/SettingsModal.module.scss | 26 - .../modals/settings-modal/SettingsModal.tsx | 55 - .../settings-modal/ViewSettingsForm.tsx | 157 - .../modals/sheets-modal/SheetsModal.tsx | 469 --- .../modals/sheets-modal/Step.module.scss | 42 - .../src/features/modals/sheets-modal/Step.tsx | 47 - .../modals/upload-modal/UploadFile.tsx | 66 - .../upload-modal/UploadModal.module.scss | 49 - .../modals/upload-modal/UploadModal.tsx | 250 -- .../preview/PreviewColumn.module.scss | 22 - .../upload-modal/preview/PreviewColumn.tsx | 0 .../upload-modal/preview/PreviewExcel.tsx | 26 - .../preview/PreviewProjectData.tsx | 26 - .../upload-modal/preview/PreviewRundown.tsx | 131 - .../preview/PreviewTable.module.scss | 71 - .../upload-entry/UploadEntry.module.scss | 53 - .../upload-modal/upload-entry/UploadEntry.tsx | 52 - .../upload-options/ExcelFileOptions.tsx | 70 - .../upload-options/ImportMapTable.module.scss | 33 - .../upload-options/ImportMapTable.tsx | 50 - .../upload-options/OntimeFileOptions.tsx | 31 - .../upload-step/UploadStep.module.scss | 40 - .../upload-modal/upload-step/UploadStep.tsx | 26 - .../upload-modal/uploadModalContext.tsx | 21 - .../modals/upload-modal/uploadUtils.ts | 64 - .../features/operator/Operator.module.scss | 8 +- .../client/src/features/operator/Operator.tsx | 54 +- .../src/features/operator/OperatorExport.tsx | 29 + .../operator/edit-modal/EditModal.module.scss | 3 - .../operator/edit-modal/EditModal.tsx | 13 +- .../follow-button/FollowButton.module.scss | 3 - .../operator-block/OperatorBlock.module.scss | 3 - .../operator-event/OperatorEvent.module.scss | 8 +- .../operator/operator-event/OperatorEvent.tsx | 25 +- .../operator/status-bar/StatusBar.module.scss | 3 - .../operator/status-bar/StatusBarProgress.tsx | 13 +- .../operator/status-bar/StatusBarTimers.tsx | 43 +- .../features/overview/Overview.module.scss | 37 + .../client/src/features/overview/Overview.tsx | 127 + .../overview/composite/TimeLayout.module.scss | 49 + .../overview/composite/TimeLayout.tsx | 36 + .../src/features/overview/overviewUtils.ts | 47 + .../src/features/rundown/Rundown.module.scss | 14 +- apps/client/src/features/rundown/Rundown.tsx | 177 +- .../src/features/rundown/RundownEmpty.tsx | 2 +- .../src/features/rundown/RundownEntry.tsx | 245 +- .../rundown/RundownExport.module.scss | 54 + .../src/features/rundown/RundownExport.tsx | 27 +- .../src/features/rundown/RundownWrapper.tsx | 14 +- .../src/features/rundown/_blockMixins.scss | 11 +- .../rundown/block-block/BlockBlock.tsx | 35 +- .../rundown/common/EditableBlockTitle.tsx | 42 +- .../rundown/common/TitleEditor.module.scss | 2 - .../delay-block/DelayBlock.module.scss | 8 +- .../rundown/delay-block/DelayBlock.tsx | 30 +- .../event-block/EventBlock.module.scss | 93 +- .../rundown/event-block/EventBlock.tsx | 168 +- .../rundown/event-block/EventBlock.utils.ts | 37 + .../rundown/event-block/EventBlockInner.tsx | 162 +- .../event-block/RundownIndicators.module.scss | 29 + .../rundown/event-block/RundownIndicators.tsx | 27 + .../__tests__/EventBlock.utils.test.ts | 48 + .../event-block/composite/BlockActionMenu.tsx | 64 - .../composite/EventBlockPlayback.tsx | 25 +- .../EventBlockProgressBar.module.scss | 19 +- .../composite/EventBlockProgressBar.tsx | 11 +- .../composite/EventBlockTimers.tsx | 90 - .../event-editor/EventEditor.module.scss | 90 + .../rundown/event-editor/EventEditor.tsx | 137 + .../composite/EventEditorTimes.tsx | 146 + .../composite/EventEditorTitles.tsx | 57 + .../event-editor/composite/EventTextArea.tsx} | 29 +- .../composite/EventTextInput.tsx} | 15 +- .../quick-add-block/QuickAddBlock.module.scss | 38 +- .../rundown/quick-add-block/QuickAddBlock.tsx | 114 +- .../rundown-header/RundownHeader.module.scss | 7 + .../rundown/rundown-header/RundownHeader.tsx | 28 + .../rundown/rundown-header/RundownMenu.tsx | 33 + .../time-input-flow/TimeInputFlow.module.scss | 30 + .../rundown/time-input-flow/TimeInputFlow.tsx | 121 + .../src/features/rundown/useEventSelection.ts | 97 + .../src/features/viewers/ViewWrapper.tsx | 124 +- .../features/viewers/backstage/Backstage.scss | 1 - .../features/viewers/backstage/Backstage.tsx | 86 +- .../src/features/viewers/clock/Clock.tsx | 26 +- .../common/{animation.js => animation.ts} | 0 .../viewers/common/clock-time/ClockTime.tsx | 25 + .../common/running-time/RunningTime.tsx | 28 + .../superscript-time/SuperscriptTime.tsx | 3 + .../src/features/viewers/common/viewUtils.ts | 101 + .../features/viewers/common/viewerUtils.ts | 43 - .../features/viewers/countdown/Countdown.tsx | 61 +- .../viewers/countdown/CountdownSelect.tsx | 12 +- .../viewers/countdown/countdown.helpers.ts | 4 +- .../viewers/lower-thirds/LowerLines.scss | 67 - .../viewers/lower-thirds/LowerLines.tsx | 180 - .../viewers/lower-thirds/LowerThird.scss | 152 + .../viewers/lower-thirds/LowerThird.tsx | 231 ++ .../viewers/lower-thirds/LowerWrapper.tsx | 137 - .../viewers/minimal-timer/MinimalTimer.tsx | 48 +- .../src/features/viewers/public/Public.tsx | 52 +- .../features/viewers/studio/StudioClock.scss | 291 +- .../features/viewers/studio/StudioClock.tsx | 120 +- .../__tests__/studioClock.utils.test.ts | 150 +- .../viewers/studio/studioClock.utils.ts | 88 +- .../src/features/viewers/timer/Timer.scss | 15 + .../src/features/viewers/timer/Timer.tsx | 111 +- apps/client/src/index.scss | 30 +- apps/client/src/index.tsx | 5 +- apps/client/src/theme/OntimeAlert.ts | 9 +- apps/client/src/theme/OntimeProgress.ts | 8 - apps/client/src/theme/_mixins.scss | 5 +- apps/client/src/theme/_ontimeColours.scss | 1 + .../{_v2Styles.scss => _ontimeStyles.scss} | 7 + apps/client/src/theme/_viewerCommon.scss | 4 - apps/client/src/theme/_viewerDefs.scss | 8 +- apps/client/src/theme/ontimeButton.ts | 41 +- apps/client/src/theme/ontimeCheckbox.ts | 14 +- apps/client/src/theme/ontimeDrawer.ts | 20 + apps/client/src/theme/ontimeMenu.ts | 22 +- apps/client/src/theme/ontimeModal.ts | 35 +- apps/client/src/theme/ontimeSelect.ts | 5 + apps/client/src/theme/ontimeSwitch.ts | 13 - apps/client/src/theme/ontimeTextInputs.ts | 41 +- apps/client/src/theme/theme.ts | 40 +- apps/client/src/translation/languages/de.ts | 1 + apps/client/src/translation/languages/en.ts | 1 + apps/client/src/translation/languages/es.ts | 1 + apps/client/src/translation/languages/fr.ts | 1 + apps/client/src/translation/languages/it.ts | 3 +- apps/client/src/translation/languages/no.ts | 1 + apps/client/src/translation/languages/pt.ts | 1 + apps/client/src/translation/languages/sv.ts | 1 + apps/client/src/viewerConfig.ts | 5 + apps/client/vite.config.js | 22 +- apps/electron/main.js | 3 +- apps/electron/package.json | 12 +- apps/electron/src/menu/applicationMenu.js | 8 +- apps/server/package.json | 39 +- apps/server/src/adapters/OscAdapter.ts | 58 +- apps/server/src/adapters/WebsocketAdapter.ts | 44 +- .../src/adapters/utils/__test__/parse.test.ts | 52 + apps/server/src/adapters/utils/parse.ts | 19 + .../custom-fields/customFields.controller.ts | 51 + .../custom-fields/customFields.router.ts | 14 + .../custom-fields/customFields.validation.ts | 45 + apps/server/src/api-data/db/db.controller.ts | 253 ++ apps/server/src/api-data/db/db.middleware.ts | 19 + apps/server/src/api-data/db/db.router.ts | 40 + apps/server/src/api-data/db/db.validation.ts | 106 + .../src/api-data/excel/excel.controller.ts | 40 + .../src/api-data/excel/excel.middleware.ts | 17 + .../server/src/api-data/excel/excel.router.ts | 16 + .../src/api-data/excel/excel.service.ts | 55 + .../src/api-data/excel/excel.validation.ts | 28 + .../src/api-data/http/http.controller.ts | 31 + apps/server/src/api-data/http/http.router.ts | 9 + .../src/api-data/http/http.validation.ts | 21 + apps/server/src/api-data/index.ts | 32 + .../server/src/api-data/osc/osc.controller.ts | 32 + apps/server/src/api-data/osc/osc.router.ts | 8 + .../server/src/api-data/osc/osc.validation.ts | 25 + .../api-data/project/project.controller.ts | 34 + .../src/api-data/project/project.router.ts | 9 + .../project/project.validation.ts} | 4 +- .../api-data/rundown/rundown.controller.ts | 130 + .../rundown/rundown.router.ts} | 26 +- .../rundown/rundown.validation.ts} | 27 +- .../api-data/settings/settings.controller.ts | 67 + .../src/api-data/settings/settings.router.ts | 8 + .../api-data/settings/settings.validation.ts | 19 + .../src/api-data/sheets/sheets.controller.ts | 117 + .../src/api-data/sheets/sheets.middleware.ts | 18 + .../src/api-data/sheets/sheets.router.ts | 28 + .../src/api-data/sheets/sheets.validation.ts | 48 + .../url-presets/urlPresets.controller.ts | 30 + .../api-data/url-presets/urlPresets.router.ts | 8 + .../url-presets/urlPresets.validation.ts | 18 + .../view-settings/viewSettings.controller.ts | 34 + .../view-settings/viewSettings.router.ts | 9 + .../view-settings/viewSettings.validation.ts | 19 + .../api-integration/integration.controller.ts | 256 ++ .../src/api-integration/integration.router.ts | 64 + .../src/api-integration/integration.utils.ts | 55 + apps/server/src/app.ts | 171 +- apps/server/src/classes/Logger.ts | 4 +- .../src/classes/data-provider/DataProvider.ts | 115 +- .../data-provider/DataProvider.utils.ts | 28 +- .../DataProvider.test.ts | 122 +- .../src/classes/event-loader/EventLoader.ts | 347 -- .../src/classes/simple-timer/SimpleTimer.ts | 78 + .../__tests__/SimpleTimer.test.ts | 129 + apps/server/src/config/config.ts | 6 + .../integrationController.config.ts | 73 - .../src/controllers/integrationController.ts | 270 -- .../src/controllers/ontimeController.ts | 578 ---- .../controllers/ontimeController.validate.ts | 183 - .../src/controllers/playbackController.ts | 98 - .../src/controllers/projectController.ts | 35 - .../src/controllers/rundownController.ts | 120 - apps/server/src/external/demo/app.js | 44 +- apps/server/src/external/demo/index.html | 4 +- apps/server/src/external/styles/override.css | 9 + apps/server/src/index.ts | 15 +- apps/server/src/models/aliasDefinition.js | 5 - apps/server/src/models/dataModel.ts | 36 +- apps/server/src/models/eventsDefinition.ts | 28 +- apps/server/src/modules/loadDb.ts | 80 - apps/server/src/preloaded-db/.keep | 1 + apps/server/src/routes/ontimeRouter.ts | 128 - apps/server/src/routes/playbackRouter.ts | 47 - apps/server/src/routes/projectRouter.ts | 11 - apps/server/src/services/Clock.ts | 3 + apps/server/src/services/PlaybackService.ts | 294 -- apps/server/src/services/RestoreService.ts | 99 +- apps/server/src/services/TimerService.ts | 657 +--- .../services/__tests__/RestoreService.test.ts | 48 +- .../src/services/__tests__/rollUtils.test.ts | 776 ----- .../src/services/__tests__/timerUtils.test.ts | 1929 +++++++++-- .../app-state-service/AppStateService.ts | 44 + apps/server/src/services/delayUtils.ts | 37 - .../extra-timer-service/ExtraTimerService.ts | 79 + .../integration-service/HttpIntegration.ts | 94 +- .../integration-service/IIntegration.ts | 25 +- .../integration-service/IntegrationService.ts | 11 +- .../integration-service/OscIntegration.ts | 195 +- .../integration-service/integrationUtils.ts | 23 +- .../message-service/MessageService.ts | 205 +- .../__tests__/MessageService.test.ts | 59 + .../__tests__/messageUtils.test.ts | 55 + .../services/message-service/messageUtils.ts | 35 + .../project-service/ProjectService.ts | 266 ++ .../__tests__/ProjectService.test.ts | 33 + .../project-service/projectFileUtils.ts | 31 + apps/server/src/services/rollUtils.ts | 203 -- .../rundown-service/RundownService.ts | 359 +- .../__tests__/delayUtils.test.ts | 16 +- .../__tests__/delayedRundown.utils.test.ts | 457 --- .../__tests__/rundownCache.test.ts | 893 +++++ .../__tests__/rundownCacheUtils.test.ts | 253 ++ .../services/rundown-service/delayUtils.ts | 136 + .../rundown-service/delayedRundown.utils.ts | 353 -- .../services/rundown-service/rundownCache.ts | 495 +++ .../rundown-service/rundownCacheUtils.ts | 135 + .../services/rundown-service/rundownUtils.ts | 119 + .../runtime-service/RuntimeService.ts | 470 +++ .../services/sheet-service/SheetService.ts | 374 ++ .../__tests__/sheetUtils.test.ts | 252 ++ .../src/services/sheet-service/sheetUtils.ts | 147 + apps/server/src/services/timerUtils.ts | 352 +- apps/server/src/settings.ts | 1 - apps/server/src/setup.ts | 106 - .../src/{config/config.js => setup/config.ts} | 14 +- apps/server/src/setup/index.ts | 146 + apps/server/src/setup/loadDb.ts | 100 + .../server/src/{modules => setup}/loadDemo.ts | 4 +- .../src/{modules => setup}/loadStyles.ts | 2 +- apps/server/src/stores/EventStore.ts | 47 +- .../src/stores/__tests__/cachingStore.test.ts | 67 - .../src/stores/__tests__/runtimeState.test.ts | 196 ++ apps/server/src/stores/cachingStore.ts | 47 - apps/server/src/stores/runtimeState.ts | 457 +++ .../utils/__tests__/fileManagement.test.ts | 28 + .../server/src/utils/__tests__/parser.test.ts | 1306 ++++--- .../utils/__tests__/parserFunctions.test.ts | 211 +- .../src/utils/__tests__/sheetUtils.test.ts | 433 --- apps/server/src/utils/__tests__/time.test.ts | 2 +- apps/server/src/utils/assert.ts | 23 + apps/server/src/utils/fileManagement.ts | 28 +- apps/server/src/utils/generateCrashReport.ts | 49 + .../src/utils/generateUniqueFilename.ts | 26 + apps/server/src/utils/networkInterfaces.ts | 24 + apps/server/src/utils/parser.ts | 514 ++- apps/server/src/utils/parserFunctions.ts | 489 ++- apps/server/src/utils/parserUtils.ts | 40 +- apps/server/src/utils/removeFileExtension.ts | 9 + .../utils/{routerUtils.js => routerUtils.ts} | 20 +- apps/server/src/utils/sheetUtils.ts | 165 - apps/server/src/utils/sheetsAuth.ts | 380 --- apps/server/src/utils/throttle.ts | 2 +- apps/server/src/utils/time.ts | 45 +- apps/server/src/utils/upload.js | 46 - apps/server/src/utils/upload.ts | 58 + apps/server/test-db/db.json | 515 +-- apps/server/tsconfig.json | 7 +- apps/test-db/db.json | 163 - demo-db/db.json | 290 +- e2e/tests/000-smoke-tests.spec.ts | 42 - e2e/tests/000-upload-showfile.spec.ts | 31 + e2e/tests/001-smoke-tests.spec.ts | 101 + e2e/tests/002-upload-showfile.spec.ts | 37 - ...on.spec.ts => 002-view-navigation.spec.ts} | 16 +- .../features/201-message-control.spec.ts | 12 +- e2e/tests/features/202-cuesheet.spec.ts | 64 +- e2e/tests/features/203-delay-block.spec.ts | 75 +- e2e/tests/features/204-editor-crud.spec.ts | 27 +- e2e/tests/features/205-operator.spec.ts | 69 +- e2e/tests/features/206-alias.spec.ts | 21 - e2e/tests/features/206-url-preset.spec.ts | 30 + e2e/tests/features/207-view-params.spec.ts | 14 + e2e/tests/features/208-auxtimer.spec.ts | 18 + .../features/301-spreadsheet-import.spec.ts | 37 + e2e/tests/fixtures/override.ts | 14 + e2e/tests/fixtures/test-db.json | 394 ++- e2e/tests/fixtures/test-sheet.xlsx | Bin 0 -> 6621 bytes e2e/tests/utils/uploadTestShowfile.ts | 25 - package.json | 14 +- packages/types/.eslintrc | 6 + packages/types/package.json | 2 +- .../ontime-controller/BackendResponse.type.ts | 22 +- .../BackendResponse.type.ts | 10 +- .../types/src/definitions/DataModel.type.ts | 22 +- .../types/src/definitions/EndAction.type.ts | 4 +- .../src/definitions/TimeStrategy.type.ts | 4 + .../src/definitions/core/CustomFields.type.ts | 10 + .../src/definitions/core/HttpSettings.type.ts | 7 +- .../src/definitions/core/OntimeEvent.type.ts | 21 +- .../src/definitions/core/OscSettings.type.ts | 13 +- .../src/definitions/core/Rundown.type.ts | 2 +- .../src/definitions/core/Settings.type.ts | 2 +- .../src/definitions/core/Subscription.type.ts | 3 - .../definitions/core/TimerLifecycle.type.ts | 1 + .../core/{Alias.type.ts => UrlPreset.type.ts} | 2 +- .../src/definitions/core/UserFields.type.ts | 12 - .../types/src/definitions/core/Views.type.ts | 11 +- .../definitions/runtime/ExtraTimer.type.ts | 17 + .../runtime/MessageControl.type.ts | 11 +- .../src/definitions/runtime/Playlist.type.ts | 8 - .../src/definitions/runtime/Runtime.type.ts | 11 + .../definitions/runtime/RuntimeStore.type.ts | 30 +- .../definitions/runtime/TimerState.type.ts | 23 +- packages/types/src/index.ts | 48 +- packages/types/src/utils/guards.ts | 14 +- packages/types/src/utils/utils.type.ts | 5 + packages/utils/.eslintrc | 9 +- packages/utils/index.ts | 60 +- packages/utils/package.json | 10 +- .../utils/src/array-utils/arrayUtils.test.ts | 36 +- .../utils/src/array-utils}/arrayUtils.ts | 15 + packages/utils/src/cue-utils/cueUtils.test.ts | 3 +- packages/utils/src/cue-utils/cueUtils.ts | 17 +- .../src/date-utils/conversionUtils.test.ts | 114 + .../utils/src/date-utils/conversionUtils.ts | 29 + .../src/date-utils/formatDisplay.test.ts | 91 - .../utils/src/date-utils/formatDisplay.ts | 31 - .../utils/src/date-utils/formatFromMillis.ts | 11 - .../utils/src/date-utils/isTimeString.test.ts | 9 + packages/utils/src/date-utils/isTimeString.ts | 12 +- .../utils/src/date-utils/millisToString.ts | 19 - ...oString.test.ts => timeFormatting.test.ts} | 42 +- .../utils/src/date-utils/timeFormatting.ts | 81 + .../src/feature/excel-import/excelImport.ts | 44 - .../__tests__/spreadsheetImport.test.ts | 50 + .../spreadsheet-import/spreadsheetImport.ts | 35 + .../utils/src/generate-id/generateId.test.ts | 26 +- packages/utils/src/generate-id/generateId.ts | 2 +- .../src/generic/__tests__/generic.test.ts | 18 + packages/utils/src/generic/generic.ts | 42 + .../src/regex-utils/isAlphanumeric.test.ts | 17 + .../utils/src/regex-utils/isAlphanumeric.ts | 8 + .../src/rundown-utils/rundownUtils.test.ts | 169 +- .../utils/src/rundown-utils/rundownUtils.ts | 339 +- .../src/validate-action/validatePlayback.ts | 8 +- .../src/validate-events/validateEvent.test.ts | 101 +- .../src/validate-events/validateEvent.ts | 91 +- .../src/validate-times/validateTimes.test.ts | 191 ++ .../utils/src/validate-times/validateTimes.ts | 130 + playwright.config.ts | 10 +- pnpm-lock.yaml | 3000 +++++++++-------- test-db/db.json | 41 + 640 files changed, 26702 insertions(+), 21623 deletions(-) create mode 100644 .github/workflows/build_v3.yml create mode 100644 .github/workflows/test_v3.yml create mode 100644 apps/client/src/assets/fonts/DSEG7ClassicMini-Regular.ttf delete mode 100644 apps/client/src/assets/fonts/digital-7.monoitalic.ttf rename apps/client/src/common/api/{apiConstants.ts => constants.ts} (79%) create mode 100644 apps/client/src/common/api/customFields.ts create mode 100644 apps/client/src/common/api/db.ts delete mode 100644 apps/client/src/common/api/eventsApi.ts create mode 100644 apps/client/src/common/api/excel.ts create mode 100644 apps/client/src/common/api/external.ts create mode 100644 apps/client/src/common/api/http.ts delete mode 100644 apps/client/src/common/api/ontimeApi.ts create mode 100644 apps/client/src/common/api/osc.ts create mode 100644 apps/client/src/common/api/project.ts delete mode 100644 apps/client/src/common/api/projectDataApi.ts create mode 100644 apps/client/src/common/api/rundown.ts create mode 100644 apps/client/src/common/api/settings.ts create mode 100644 apps/client/src/common/api/sheets.ts create mode 100644 apps/client/src/common/api/urlPresets.ts rename apps/client/src/common/api/{apiUtils.ts => utils.ts} (57%) create mode 100644 apps/client/src/common/api/viewSettings.ts create mode 100644 apps/client/src/common/components/app-link/AppLink.module.scss create mode 100644 apps/client/src/common/components/app-link/AppLink.tsx delete mode 100644 apps/client/src/common/components/collapse-bar/CollapseBar.module.scss delete mode 100644 apps/client/src/common/components/collapse-bar/CollapseBar.tsx create mode 100644 apps/client/src/common/components/context-menu/ContextMenuOption.tsx rename apps/client/src/{features/modals/ModalLink.module.scss => common/components/external-link/ExternalLink.module.scss} (73%) rename apps/client/src/{features/modals/ModalLink.tsx => common/components/external-link/ExternalLink.tsx} (69%) create mode 100644 apps/client/src/common/components/input/time-input/TimeInputWithButton.module.scss create mode 100644 apps/client/src/common/components/input/time-input/TimeInputWithButton.tsx rename apps/client/src/{features/modals/modal-loader/ModalLoader.module.scss => common/components/loader-overlay/LoaderOverlay.module.scss} (83%) create mode 100644 apps/client/src/common/components/loader-overlay/LoaderOverlay.tsx create mode 100644 apps/client/src/common/components/navigation-menu/FloatingNavigation.tsx create mode 100644 apps/client/src/common/components/navigation-menu/ProductionNavigationMenu.tsx create mode 100644 apps/client/src/common/components/navigation-menu/ViewNavigationMenu.tsx delete mode 100644 apps/client/src/common/components/navigation-menu/rename-client-modal/RenameClientModal.module.scss rename apps/client/src/{features/modals/upload-modal/preview => common/components/tag}/Tag.module.scss (57%) rename apps/client/src/{features/modals/upload-modal/preview => common/components/tag}/Tag.tsx (100%) delete mode 100644 apps/client/src/common/components/timer-display/TimerDisplay.tsx delete mode 100644 apps/client/src/common/hooks-query/useAliases.ts create mode 100644 apps/client/src/common/hooks-query/useCustomFields.ts create mode 100644 apps/client/src/common/hooks-query/useProjectList.ts create mode 100644 apps/client/src/common/hooks-query/useUrlPresets.ts delete mode 100644 apps/client/src/common/hooks-query/useUserFields.ts delete mode 100644 apps/client/src/common/hooks/useFullscreen.ts create mode 100644 apps/client/src/common/hooks/useMemoisedFn.ts create mode 100644 apps/client/src/common/hooks/useScrollIntoView.tsx create mode 100644 apps/client/src/common/hooks/useWindowTitle.ts delete mode 100644 apps/client/src/common/models/UserFields.ts rename apps/client/src/common/utils/__tests__/{aliases.test.js => urlPresets.test.js} (65%) delete mode 100644 apps/client/src/common/utils/aliases.ts delete mode 100644 apps/client/src/common/utils/fileDownload.ts create mode 100644 apps/client/src/common/utils/keyEvent.ts delete mode 100644 apps/client/src/common/utils/timeConstants.js delete mode 100644 apps/client/src/common/utils/timesManager.ts create mode 100644 apps/client/src/common/utils/uploadUtils.ts create mode 100644 apps/client/src/common/utils/urlPresets.ts delete mode 100644 apps/client/src/common/utils/viewUtils.ts rename apps/client/src/features/{AliasWrapper.tsx => PresetWrapper.tsx} (64%) create mode 100644 apps/client/src/features/app-settings/AppSettings.module.scss create mode 100644 apps/client/src/features/app-settings/AppSettings.tsx create mode 100644 apps/client/src/features/app-settings/panel-content/PanelContent.module.scss create mode 100644 apps/client/src/features/app-settings/panel-content/PanelContent.tsx create mode 100644 apps/client/src/features/app-settings/panel-list/PanelList.module.scss create mode 100644 apps/client/src/features/app-settings/panel-list/PanelList.tsx create mode 100644 apps/client/src/features/app-settings/panel/Panel.module.scss create mode 100644 apps/client/src/features/app-settings/panel/PanelUtils.tsx create mode 100644 apps/client/src/features/app-settings/panel/about-panel/AboutPanel.tsx rename apps/client/src/features/{modals/about-modal/UpdateChecker.tsx => app-settings/panel/about-panel/CheckUpdatesButton.tsx} (65%) create mode 100644 apps/client/src/features/app-settings/panel/feature-settings-panel/FeatureSettings.module.scss create mode 100644 apps/client/src/features/app-settings/panel/feature-settings-panel/FeatureSettingsPanel.tsx create mode 100644 apps/client/src/features/app-settings/panel/feature-settings-panel/UrlPresetsForm.tsx create mode 100644 apps/client/src/features/app-settings/panel/feature-settings-panel/custom-fields/CustomFieldEntry.tsx create mode 100644 apps/client/src/features/app-settings/panel/feature-settings-panel/custom-fields/CustomFieldForm.tsx create mode 100644 apps/client/src/features/app-settings/panel/feature-settings-panel/custom-fields/CustomFields.tsx create mode 100644 apps/client/src/features/app-settings/panel/general-panel/GeneralPanel.module.scss create mode 100644 apps/client/src/features/app-settings/panel/general-panel/GeneralPanel.tsx create mode 100644 apps/client/src/features/app-settings/panel/general-panel/GeneralPanelForm.tsx rename apps/client/src/features/{modals/settings-modal/ModalPinInput.tsx => app-settings/panel/general-panel/GeneralPinInput.tsx} (66%) create mode 100644 apps/client/src/features/app-settings/panel/general-panel/ViewSettingsForm.tsx create mode 100644 apps/client/src/features/app-settings/panel/integrations-panel/HttpIntegrations.tsx create mode 100644 apps/client/src/features/app-settings/panel/integrations-panel/IntegrationsPanel.module.css create mode 100644 apps/client/src/features/app-settings/panel/integrations-panel/IntegrationsPanel.tsx create mode 100644 apps/client/src/features/app-settings/panel/integrations-panel/OscIntegrations.tsx create mode 100644 apps/client/src/features/app-settings/panel/integrations-panel/integrationUtils.ts create mode 100644 apps/client/src/features/app-settings/panel/interface-panel/EditorSettingsForm.tsx create mode 100644 apps/client/src/features/app-settings/panel/interface-panel/InterfacePanel.tsx create mode 100644 apps/client/src/features/app-settings/panel/log-panel/LogExport.module.scss create mode 100644 apps/client/src/features/app-settings/panel/log-panel/LogExport.tsx create mode 100644 apps/client/src/features/app-settings/panel/log-panel/LogPanel.tsx create mode 100644 apps/client/src/features/app-settings/panel/log-panel/NetworkInterfaces.module.scss create mode 100644 apps/client/src/features/app-settings/panel/log-panel/NetworkInterfaces.tsx create mode 100644 apps/client/src/features/app-settings/panel/project-panel/ManageProjects.tsx create mode 100644 apps/client/src/features/app-settings/panel/project-panel/ProjectCreateForm.tsx create mode 100644 apps/client/src/features/app-settings/panel/project-panel/ProjectData.tsx create mode 100644 apps/client/src/features/app-settings/panel/project-panel/ProjectForm.tsx create mode 100644 apps/client/src/features/app-settings/panel/project-panel/ProjectList.tsx create mode 100644 apps/client/src/features/app-settings/panel/project-panel/ProjectListItem.tsx create mode 100644 apps/client/src/features/app-settings/panel/project-panel/ProjectPanel.module.scss create mode 100644 apps/client/src/features/app-settings/panel/project-panel/ProjectPanel.tsx create mode 100644 apps/client/src/features/app-settings/panel/shutdown-panel/ShutdownPanel.tsx create mode 100644 apps/client/src/features/app-settings/panel/sources-panel/GSheetInfo.tsx create mode 100644 apps/client/src/features/app-settings/panel/sources-panel/GSheetSetup.tsx create mode 100644 apps/client/src/features/app-settings/panel/sources-panel/ImportReview.tsx create mode 100644 apps/client/src/features/app-settings/panel/sources-panel/SourcesPanel.module.scss create mode 100644 apps/client/src/features/app-settings/panel/sources-panel/SourcesPanel.tsx create mode 100644 apps/client/src/features/app-settings/panel/sources-panel/import-map/ImportMapForm.tsx create mode 100644 apps/client/src/features/app-settings/panel/sources-panel/import-map/__test__/importMapUtils.test.ts create mode 100644 apps/client/src/features/app-settings/panel/sources-panel/import-map/importMapUtils.ts create mode 100644 apps/client/src/features/app-settings/panel/sources-panel/preview/PreviewRundown.module.scss create mode 100644 apps/client/src/features/app-settings/panel/sources-panel/preview/PreviewRundown.tsx create mode 100644 apps/client/src/features/app-settings/panel/sources-panel/useGoogleSheet.ts create mode 100644 apps/client/src/features/app-settings/panel/sources-panel/useSheetStore.ts create mode 100644 apps/client/src/features/app-settings/settingsStore.ts create mode 100644 apps/client/src/features/app-settings/useAppSettingsNavigation.tsx create mode 100644 apps/client/src/features/control/playback/extra-timer/ExtraTimer.module.scss create mode 100644 apps/client/src/features/control/playback/extra-timer/ExtraTimer.tsx rename apps/client/src/{common/components/timer-display/TimerDisplay.scss => features/control/playback/timer-display/TimerDisplay.module.scss} (66%) create mode 100644 apps/client/src/features/control/playback/timer-display/TimerDisplay.tsx create mode 100644 apps/client/src/features/editors/EditorMixin.scss delete mode 100644 apps/client/src/features/event-editor/EventEditor.module.scss delete mode 100644 apps/client/src/features/event-editor/EventEditor.tsx delete mode 100644 apps/client/src/features/event-editor/EventEditorExport.tsx delete mode 100644 apps/client/src/features/event-editor/composite/EventEditorDataLeft.tsx delete mode 100644 apps/client/src/features/event-editor/composite/EventEditorDataRight.tsx delete mode 100644 apps/client/src/features/event-editor/composite/EventEditorTimes.tsx delete mode 100644 apps/client/src/features/info/CollapsableInfo.tsx delete mode 100644 apps/client/src/features/info/Info.module.scss delete mode 100644 apps/client/src/features/info/Info.tsx delete mode 100644 apps/client/src/features/info/InfoExport.tsx delete mode 100644 apps/client/src/features/info/InfoNif.tsx delete mode 100644 apps/client/src/features/info/InfoTitles.tsx delete mode 100644 apps/client/src/features/info/info-header/InfoHeader.tsx rename apps/client/src/features/{info/InfoLogger.module.scss => log/Log.module.scss} (63%) rename apps/client/src/features/{info/InfoLogger.tsx => log/Log.tsx} (86%) delete mode 100644 apps/client/src/features/menu/MenuBar.module.scss delete mode 100644 apps/client/src/features/menu/MenuBar.tsx delete mode 100644 apps/client/src/features/menu/RundownMenu.module.scss delete mode 100644 apps/client/src/features/menu/RundownMenu.tsx delete mode 100644 apps/client/src/features/modals/Modal.module.scss delete mode 100644 apps/client/src/features/modals/ModalInput.tsx delete mode 100644 apps/client/src/features/modals/ModalSplitInput.tsx delete mode 100644 apps/client/src/features/modals/ModalWrapper.tsx delete mode 100644 apps/client/src/features/modals/OntimeModalFooter.tsx delete mode 100644 apps/client/src/features/modals/about-modal/AboutModal.tsx delete mode 100644 apps/client/src/features/modals/export-modal/ExportModal.module.scss delete mode 100644 apps/client/src/features/modals/export-modal/ExportModal.tsx delete mode 100644 apps/client/src/features/modals/integration-modal/IntegrationModal.tsx delete mode 100644 apps/client/src/features/modals/integration-modal/http/HttpIntegration.tsx delete mode 100644 apps/client/src/features/modals/integration-modal/http/HttpSubscriptionRow.tsx delete mode 100644 apps/client/src/features/modals/integration-modal/integration.utils.ts delete mode 100644 apps/client/src/features/modals/integration-modal/osc/OscIntegration.tsx delete mode 100644 apps/client/src/features/modals/integration-modal/osc/OscSettings.tsx delete mode 100644 apps/client/src/features/modals/integration-modal/osc/OscSubscriptionRow.tsx delete mode 100644 apps/client/src/features/modals/modal-loader/ModalLoader.tsx delete mode 100644 apps/client/src/features/modals/modalHelper.js delete mode 100644 apps/client/src/features/modals/quick-start/QuickStart.tsx delete mode 100644 apps/client/src/features/modals/settings-modal/AliasesForm.tsx delete mode 100644 apps/client/src/features/modals/settings-modal/AppSettings.tsx delete mode 100644 apps/client/src/features/modals/settings-modal/CuesheetSettingsForm.tsx delete mode 100644 apps/client/src/features/modals/settings-modal/EditorSettings.tsx delete mode 100644 apps/client/src/features/modals/settings-modal/InputMillisWithString.tsx delete mode 100644 apps/client/src/features/modals/settings-modal/ProjectDataForm.tsx delete mode 100644 apps/client/src/features/modals/settings-modal/SettingsModal.module.scss delete mode 100644 apps/client/src/features/modals/settings-modal/SettingsModal.tsx delete mode 100644 apps/client/src/features/modals/settings-modal/ViewSettingsForm.tsx delete mode 100644 apps/client/src/features/modals/sheets-modal/SheetsModal.tsx delete mode 100644 apps/client/src/features/modals/sheets-modal/Step.module.scss delete mode 100644 apps/client/src/features/modals/sheets-modal/Step.tsx delete mode 100644 apps/client/src/features/modals/upload-modal/UploadFile.tsx delete mode 100644 apps/client/src/features/modals/upload-modal/UploadModal.module.scss delete mode 100644 apps/client/src/features/modals/upload-modal/UploadModal.tsx delete mode 100644 apps/client/src/features/modals/upload-modal/preview/PreviewColumn.module.scss delete mode 100644 apps/client/src/features/modals/upload-modal/preview/PreviewColumn.tsx delete mode 100644 apps/client/src/features/modals/upload-modal/preview/PreviewExcel.tsx delete mode 100644 apps/client/src/features/modals/upload-modal/preview/PreviewProjectData.tsx delete mode 100644 apps/client/src/features/modals/upload-modal/preview/PreviewRundown.tsx delete mode 100644 apps/client/src/features/modals/upload-modal/preview/PreviewTable.module.scss delete mode 100644 apps/client/src/features/modals/upload-modal/upload-entry/UploadEntry.module.scss delete mode 100644 apps/client/src/features/modals/upload-modal/upload-entry/UploadEntry.tsx delete mode 100644 apps/client/src/features/modals/upload-modal/upload-options/ExcelFileOptions.tsx delete mode 100644 apps/client/src/features/modals/upload-modal/upload-options/ImportMapTable.module.scss delete mode 100644 apps/client/src/features/modals/upload-modal/upload-options/ImportMapTable.tsx delete mode 100644 apps/client/src/features/modals/upload-modal/upload-options/OntimeFileOptions.tsx delete mode 100644 apps/client/src/features/modals/upload-modal/upload-step/UploadStep.module.scss delete mode 100644 apps/client/src/features/modals/upload-modal/upload-step/UploadStep.tsx delete mode 100644 apps/client/src/features/modals/upload-modal/uploadModalContext.tsx delete mode 100644 apps/client/src/features/modals/upload-modal/uploadUtils.ts create mode 100644 apps/client/src/features/operator/OperatorExport.tsx create mode 100644 apps/client/src/features/overview/Overview.module.scss create mode 100644 apps/client/src/features/overview/Overview.tsx create mode 100644 apps/client/src/features/overview/composite/TimeLayout.module.scss create mode 100644 apps/client/src/features/overview/composite/TimeLayout.tsx create mode 100644 apps/client/src/features/overview/overviewUtils.ts create mode 100644 apps/client/src/features/rundown/RundownExport.module.scss create mode 100644 apps/client/src/features/rundown/event-block/EventBlock.utils.ts create mode 100644 apps/client/src/features/rundown/event-block/RundownIndicators.module.scss create mode 100644 apps/client/src/features/rundown/event-block/RundownIndicators.tsx create mode 100644 apps/client/src/features/rundown/event-block/__tests__/EventBlock.utils.test.ts delete mode 100644 apps/client/src/features/rundown/event-block/composite/BlockActionMenu.tsx delete mode 100644 apps/client/src/features/rundown/event-block/composite/EventBlockTimers.tsx create mode 100644 apps/client/src/features/rundown/event-editor/EventEditor.module.scss create mode 100644 apps/client/src/features/rundown/event-editor/EventEditor.tsx create mode 100644 apps/client/src/features/rundown/event-editor/composite/EventEditorTimes.tsx create mode 100644 apps/client/src/features/rundown/event-editor/composite/EventEditorTitles.tsx rename apps/client/src/features/{event-editor/composite/CountedTextArea.tsx => rundown/event-editor/composite/EventTextArea.tsx} (51%) rename apps/client/src/features/{event-editor/composite/CountedTextInput.tsx => rundown/event-editor/composite/EventTextInput.tsx} (70%) create mode 100644 apps/client/src/features/rundown/rundown-header/RundownHeader.module.scss create mode 100644 apps/client/src/features/rundown/rundown-header/RundownHeader.tsx create mode 100644 apps/client/src/features/rundown/rundown-header/RundownMenu.tsx create mode 100644 apps/client/src/features/rundown/time-input-flow/TimeInputFlow.module.scss create mode 100644 apps/client/src/features/rundown/time-input-flow/TimeInputFlow.tsx create mode 100644 apps/client/src/features/rundown/useEventSelection.ts rename apps/client/src/features/viewers/common/{animation.js => animation.ts} (100%) create mode 100644 apps/client/src/features/viewers/common/clock-time/ClockTime.tsx create mode 100644 apps/client/src/features/viewers/common/running-time/RunningTime.tsx create mode 100644 apps/client/src/features/viewers/common/viewUtils.ts delete mode 100644 apps/client/src/features/viewers/common/viewerUtils.ts delete mode 100644 apps/client/src/features/viewers/lower-thirds/LowerLines.scss delete mode 100644 apps/client/src/features/viewers/lower-thirds/LowerLines.tsx create mode 100644 apps/client/src/features/viewers/lower-thirds/LowerThird.scss create mode 100644 apps/client/src/features/viewers/lower-thirds/LowerThird.tsx delete mode 100644 apps/client/src/features/viewers/lower-thirds/LowerWrapper.tsx delete mode 100644 apps/client/src/theme/OntimeProgress.ts rename apps/client/src/theme/{_v2Styles.scss => _ontimeStyles.scss} (91%) create mode 100644 apps/client/src/theme/ontimeDrawer.ts create mode 100644 apps/server/src/adapters/utils/__test__/parse.test.ts create mode 100644 apps/server/src/adapters/utils/parse.ts create mode 100644 apps/server/src/api-data/custom-fields/customFields.controller.ts create mode 100644 apps/server/src/api-data/custom-fields/customFields.router.ts create mode 100644 apps/server/src/api-data/custom-fields/customFields.validation.ts create mode 100644 apps/server/src/api-data/db/db.controller.ts create mode 100644 apps/server/src/api-data/db/db.middleware.ts create mode 100644 apps/server/src/api-data/db/db.router.ts create mode 100644 apps/server/src/api-data/db/db.validation.ts create mode 100644 apps/server/src/api-data/excel/excel.controller.ts create mode 100644 apps/server/src/api-data/excel/excel.middleware.ts create mode 100644 apps/server/src/api-data/excel/excel.router.ts create mode 100644 apps/server/src/api-data/excel/excel.service.ts create mode 100644 apps/server/src/api-data/excel/excel.validation.ts create mode 100644 apps/server/src/api-data/http/http.controller.ts create mode 100644 apps/server/src/api-data/http/http.router.ts create mode 100644 apps/server/src/api-data/http/http.validation.ts create mode 100644 apps/server/src/api-data/index.ts create mode 100644 apps/server/src/api-data/osc/osc.controller.ts create mode 100644 apps/server/src/api-data/osc/osc.router.ts create mode 100644 apps/server/src/api-data/osc/osc.validation.ts create mode 100644 apps/server/src/api-data/project/project.controller.ts create mode 100644 apps/server/src/api-data/project/project.router.ts rename apps/server/src/{controllers/projectController.validate.ts => api-data/project/project.validation.ts} (83%) create mode 100644 apps/server/src/api-data/rundown/rundown.controller.ts rename apps/server/src/{routes/rundownRouter.ts => api-data/rundown/rundown.router.ts} (52%) rename apps/server/src/{controllers/rundownController.validate.ts => api-data/rundown/rundown.validation.ts} (66%) create mode 100644 apps/server/src/api-data/settings/settings.controller.ts create mode 100644 apps/server/src/api-data/settings/settings.router.ts create mode 100644 apps/server/src/api-data/settings/settings.validation.ts create mode 100644 apps/server/src/api-data/sheets/sheets.controller.ts create mode 100644 apps/server/src/api-data/sheets/sheets.middleware.ts create mode 100644 apps/server/src/api-data/sheets/sheets.router.ts create mode 100644 apps/server/src/api-data/sheets/sheets.validation.ts create mode 100644 apps/server/src/api-data/url-presets/urlPresets.controller.ts create mode 100644 apps/server/src/api-data/url-presets/urlPresets.router.ts create mode 100644 apps/server/src/api-data/url-presets/urlPresets.validation.ts create mode 100644 apps/server/src/api-data/view-settings/viewSettings.controller.ts create mode 100644 apps/server/src/api-data/view-settings/viewSettings.router.ts create mode 100644 apps/server/src/api-data/view-settings/viewSettings.validation.ts create mode 100644 apps/server/src/api-integration/integration.controller.ts create mode 100644 apps/server/src/api-integration/integration.router.ts create mode 100644 apps/server/src/api-integration/integration.utils.ts rename apps/server/src/classes/data-provider/{__test__ => __tests__}/DataProvider.test.ts (69%) delete mode 100644 apps/server/src/classes/event-loader/EventLoader.ts create mode 100644 apps/server/src/classes/simple-timer/SimpleTimer.ts create mode 100644 apps/server/src/classes/simple-timer/__tests__/SimpleTimer.test.ts create mode 100644 apps/server/src/config/config.ts delete mode 100644 apps/server/src/controllers/integrationController.config.ts delete mode 100644 apps/server/src/controllers/integrationController.ts delete mode 100644 apps/server/src/controllers/ontimeController.ts delete mode 100644 apps/server/src/controllers/ontimeController.validate.ts delete mode 100644 apps/server/src/controllers/playbackController.ts delete mode 100644 apps/server/src/controllers/projectController.ts delete mode 100644 apps/server/src/controllers/rundownController.ts delete mode 100644 apps/server/src/models/aliasDefinition.js delete mode 100644 apps/server/src/modules/loadDb.ts delete mode 100644 apps/server/src/routes/ontimeRouter.ts delete mode 100644 apps/server/src/routes/playbackRouter.ts delete mode 100644 apps/server/src/routes/projectRouter.ts delete mode 100644 apps/server/src/services/PlaybackService.ts delete mode 100644 apps/server/src/services/__tests__/rollUtils.test.ts create mode 100644 apps/server/src/services/app-state-service/AppStateService.ts delete mode 100644 apps/server/src/services/delayUtils.ts create mode 100644 apps/server/src/services/extra-timer-service/ExtraTimerService.ts create mode 100644 apps/server/src/services/message-service/__tests__/MessageService.test.ts create mode 100644 apps/server/src/services/message-service/__tests__/messageUtils.test.ts create mode 100644 apps/server/src/services/message-service/messageUtils.ts create mode 100644 apps/server/src/services/project-service/ProjectService.ts create mode 100644 apps/server/src/services/project-service/__tests__/ProjectService.test.ts create mode 100644 apps/server/src/services/project-service/projectFileUtils.ts delete mode 100644 apps/server/src/services/rollUtils.ts rename apps/server/src/services/{ => rundown-service}/__tests__/delayUtils.test.ts (94%) delete mode 100644 apps/server/src/services/rundown-service/__tests__/delayedRundown.utils.test.ts create mode 100644 apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts create mode 100644 apps/server/src/services/rundown-service/__tests__/rundownCacheUtils.test.ts create mode 100644 apps/server/src/services/rundown-service/delayUtils.ts delete mode 100644 apps/server/src/services/rundown-service/delayedRundown.utils.ts create mode 100644 apps/server/src/services/rundown-service/rundownCache.ts create mode 100644 apps/server/src/services/rundown-service/rundownCacheUtils.ts create mode 100644 apps/server/src/services/rundown-service/rundownUtils.ts create mode 100644 apps/server/src/services/runtime-service/RuntimeService.ts create mode 100644 apps/server/src/services/sheet-service/SheetService.ts create mode 100644 apps/server/src/services/sheet-service/__tests__/sheetUtils.test.ts create mode 100644 apps/server/src/services/sheet-service/sheetUtils.ts delete mode 100644 apps/server/src/settings.ts delete mode 100644 apps/server/src/setup.ts rename apps/server/src/{config/config.js => setup/config.ts} (72%) create mode 100644 apps/server/src/setup/index.ts create mode 100644 apps/server/src/setup/loadDb.ts rename apps/server/src/{modules => setup}/loadDemo.ts (90%) rename apps/server/src/{modules => setup}/loadStyles.ts (95%) delete mode 100644 apps/server/src/stores/__tests__/cachingStore.test.ts create mode 100644 apps/server/src/stores/__tests__/runtimeState.test.ts delete mode 100644 apps/server/src/stores/cachingStore.ts create mode 100644 apps/server/src/stores/runtimeState.ts create mode 100644 apps/server/src/utils/__tests__/fileManagement.test.ts delete mode 100644 apps/server/src/utils/__tests__/sheetUtils.test.ts create mode 100644 apps/server/src/utils/assert.ts create mode 100644 apps/server/src/utils/generateCrashReport.ts create mode 100644 apps/server/src/utils/generateUniqueFilename.ts create mode 100644 apps/server/src/utils/networkInterfaces.ts create mode 100644 apps/server/src/utils/removeFileExtension.ts rename apps/server/src/utils/{routerUtils.js => routerUtils.ts} (67%) delete mode 100644 apps/server/src/utils/sheetUtils.ts delete mode 100644 apps/server/src/utils/sheetsAuth.ts delete mode 100644 apps/server/src/utils/upload.js create mode 100644 apps/server/src/utils/upload.ts delete mode 100644 apps/test-db/db.json delete mode 100644 e2e/tests/000-smoke-tests.spec.ts create mode 100644 e2e/tests/000-upload-showfile.spec.ts create mode 100644 e2e/tests/001-smoke-tests.spec.ts delete mode 100644 e2e/tests/002-upload-showfile.spec.ts rename e2e/tests/{001-view-navigation.spec.ts => 002-view-navigation.spec.ts} (84%) delete mode 100644 e2e/tests/features/206-alias.spec.ts create mode 100644 e2e/tests/features/206-url-preset.spec.ts create mode 100644 e2e/tests/features/207-view-params.spec.ts create mode 100644 e2e/tests/features/208-auxtimer.spec.ts create mode 100644 e2e/tests/features/301-spreadsheet-import.spec.ts create mode 100644 e2e/tests/fixtures/override.ts create mode 100644 e2e/tests/fixtures/test-sheet.xlsx delete mode 100644 e2e/tests/utils/uploadTestShowfile.ts create mode 100644 packages/types/src/definitions/TimeStrategy.type.ts create mode 100644 packages/types/src/definitions/core/CustomFields.type.ts delete mode 100644 packages/types/src/definitions/core/Subscription.type.ts rename packages/types/src/definitions/core/{Alias.type.ts => UrlPreset.type.ts} (71%) delete mode 100644 packages/types/src/definitions/core/UserFields.type.ts create mode 100644 packages/types/src/definitions/runtime/ExtraTimer.type.ts delete mode 100644 packages/types/src/definitions/runtime/Playlist.type.ts create mode 100644 packages/types/src/definitions/runtime/Runtime.type.ts rename apps/server/src/utils/__tests__/arrayUtils.tests.ts => packages/utils/src/array-utils/arrayUtils.test.ts (63%) rename {apps/server/src/utils => packages/utils/src/array-utils}/arrayUtils.ts (69%) create mode 100644 packages/utils/src/date-utils/conversionUtils.test.ts create mode 100644 packages/utils/src/date-utils/conversionUtils.ts delete mode 100644 packages/utils/src/date-utils/formatDisplay.test.ts delete mode 100644 packages/utils/src/date-utils/formatDisplay.ts delete mode 100644 packages/utils/src/date-utils/formatFromMillis.ts delete mode 100644 packages/utils/src/date-utils/millisToString.ts rename packages/utils/src/date-utils/{millisToString.test.ts => timeFormatting.test.ts} (58%) create mode 100644 packages/utils/src/date-utils/timeFormatting.ts delete mode 100644 packages/utils/src/feature/excel-import/excelImport.ts create mode 100644 packages/utils/src/feature/spreadsheet-import/__tests__/spreadsheetImport.test.ts create mode 100644 packages/utils/src/feature/spreadsheet-import/spreadsheetImport.ts create mode 100644 packages/utils/src/generic/__tests__/generic.test.ts create mode 100644 packages/utils/src/generic/generic.ts create mode 100644 packages/utils/src/regex-utils/isAlphanumeric.test.ts create mode 100644 packages/utils/src/regex-utils/isAlphanumeric.ts create mode 100644 packages/utils/src/validate-times/validateTimes.test.ts create mode 100644 packages/utils/src/validate-times/validateTimes.ts create mode 100644 test-db/db.json diff --git a/.eslintrc b/.eslintrc index 23d1da5c3..7e5b262e8 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,37 +8,28 @@ "jest": true }, "parser": "@typescript-eslint/parser", - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "prettier", - "eslint-config-prettier" - ], - "plugins": [ - "@typescript-eslint", - "prettier" - ], + "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier", "eslint-config-prettier"], + "plugins": ["@typescript-eslint", "prettier"], "overrides": [ { - "files": [ - "e2e/**/**.spec.ts", - "e2e/**/**.test.ts" - ], - "extends": [ - "plugin:playwright/playwright-test" - ] + "files": ["e2e/**/**.spec.ts", "e2e/**/**.test.ts"], + "extends": ["plugin:playwright/playwright-test"] } ], "rules": { "no-useless-concat": "warn", "prefer-template": "warn", + "no-throw-literal": "error", "no-console": [ "warn", { - "allow": [ - "warn", - "error" - ] + "allow": ["warn", "error"] + } + ], + "no-restricted-imports": [ + "error", + { + "patterns": ["ontime-types/src/*", "ontime-utils/src/*"] } ], "@typescript-eslint/no-non-null-assertion": "warn", @@ -59,4 +50,4 @@ } ] } -} \ No newline at end of file +} diff --git a/.github/workflows/build_v2.yml b/.github/workflows/build_v2.yml index 8beae6bc8..9b7baed55 100644 --- a/.github/workflows/build_v2.yml +++ b/.github/workflows/build_v2.yml @@ -14,7 +14,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18.18.2 - name: Setup pnpm uses: pnpm/action-setup@v2 @@ -49,7 +49,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18.18.2 - name: Setup pnpm uses: pnpm/action-setup@v2 @@ -82,7 +82,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18.18.2 - name: Setup pnpm uses: pnpm/action-setup@v2 diff --git a/.github/workflows/build_v3.yml b/.github/workflows/build_v3.yml new file mode 100644 index 000000000..909bf8468 --- /dev/null +++ b/.github/workflows/build_v3.yml @@ -0,0 +1,109 @@ +name: Ontime build v3 + +on: +# Only trigger manually for now for testing +# push: +# tags: [ "*" ] + workflow_dispatch: + +jobs: + build_macos: + runs-on: macOS-latest + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: 18.18.2 + + - name: Setup pnpm + uses: pnpm/action-setup@v3 + with: + version: 8 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build project packages + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + run: pnpm build + + - name: Electron - Build app + run: pnpm dist-mac + + - name: Release + uses: softprops/action-gh-release@v1 + with: + files: | + ./apps/electron/dist/ontime-macOS-x64.dmg + ./apps/electron/dist/ontime-macOS-arm64.dmg + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + build_windows: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: 18.18.2 + + - name: Setup pnpm + uses: pnpm/action-setup@v3 + with: + version: 8 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build project packages + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + run: pnpm build + + - name: Electron - Build app + run: pnpm dist-win + + - name: Release + uses: softprops/action-gh-release@v1 + with: + files: './apps/electron/dist/ontime-win64.exe' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + build_ubuntu: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: 18.18.2 + + - name: Setup pnpm + uses: pnpm/action-setup@v3 + with: + version: 8 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build project packages + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + run: pnpm build + + - name: Electron - Build app + run: pnpm dist-linux + + - name: Release + uses: softprops/action-gh-release@v1 + with: + files: './apps/electron/dist/ontime-linux.AppImage' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test_v2.yml b/.github/workflows/test_v2.yml index ac849f32d..96b537154 100644 --- a/.github/workflows/test_v2.yml +++ b/.github/workflows/test_v2.yml @@ -17,7 +17,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18.18.2 - name: Setup pnpm uses: pnpm/action-setup@v2 @@ -73,7 +73,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18.18.2 - name: Setup pnpm uses: pnpm/action-setup@v2 @@ -98,3 +98,9 @@ jobs: name: playwright-report path: playwright-report/ retention-days: 30 + + - uses: actions/upload-artifact@v3 + with: + name: automated-screenshots + path: automated-screenshots/ + retention-days: 14 diff --git a/.github/workflows/test_v3.yml b/.github/workflows/test_v3.yml new file mode 100644 index 000000000..36f339f7e --- /dev/null +++ b/.github/workflows/test_v3.yml @@ -0,0 +1,107 @@ +name: Ontime test v3 + +on: +# Only trigger manually for now for testing +# pull_request: +# branches: '*' + workflow_dispatch: + +jobs: + unit-test: + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + CI: '' + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: 18.18.2 + + - name: Setup pnpm + uses: pnpm/action-setup@v3 + with: + version: 8 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + # Run code quality per package + - name: React - Run linter + TypeScript checks + if: always() + run: pnpm lint && tsc --noEmit + working-directory: ./apps/client + + - name: Server - Run linter + TypeScript checks + if: always() + run: pnpm lint && tsc --noEmit + working-directory: ./apps/server + + - name: Utils - Run linter + TypeScript checks + if: always() + run: pnpm lint && tsc --noEmit + working-directory: ./packages/utils + + - name: Types - Run linter + if: always() + run: pnpm lint + working-directory: ./packages/types + + # We choose to run tests separately + - name: React - Run unit tests + if: always() + run: pnpm test:pipeline + working-directory: ./apps/client + + - name: Server - Run unit tests + if: always() + run: pnpm test:pipeline + working-directory: ./apps/server + + - name: Utils - Run unit tests + if: always() + run: pnpm test:pipeline + working-directory: ./packages/utils + + e2e-test: + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: 18.18.2 + + - name: Setup pnpm + uses: pnpm/action-setup@v3 + with: + version: 8 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build client + run: pnpm build:local + + - name: Install Playwright Browsers + run: npx playwright install --with-deps + + - name: Run Playwright tests + run: pnpm e2e + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 + + - uses: actions/upload-artifact@v4 + with: + name: automated-screenshots + path: automated-screenshots/ + retention-days: 14 diff --git a/.gitignore b/.gitignore index 07063a3a5..4e09da916 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ ontime.code-workspace test-results playwright-report /playwright/.cache/ +automated-screenshots # production build/ @@ -44,3 +45,6 @@ apps/server/src/preloaded-db/db.json # versioning file **/ONTIME_VERSION.js + +# temporary write files +**.tmp \ No newline at end of file diff --git a/.nvmrc b/.nvmrc index 5d8bda1da..68c98aa7a 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v16.16.0 \ No newline at end of file +v18.18.2 \ No newline at end of file diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 0af5f3fd0..ef8afadea 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -9,8 +9,8 @@ Ontime consists of 3 distinct parts The steps below will assume you have locally installed the necessary dependencies. Other dependencies will be installed as part of the setup -- __node__ (>=16.16) -- __pnpm__ (>=7) +- __node__ (~18.18.2) +- __pnpm__ (>=8) - __docker__ (only necessary to run and build docker images) ## LOCAL DEVELOPMENT @@ -97,3 +97,10 @@ Other useful commands - __List running processes__ by running `docker ps` - __Kill running process__ by running `docker kill ` + +## General Info + +# APP Building + +We build the app from app.js for almost all applications. The output file will still be named index.cjs. This is because of Electron. +Building the app from index.ts only applies for applications that don't use electron. index.ts will take over the initialization of the server and UI when electron isn't present. diff --git a/Dockerfile b/Dockerfile index 65df28fe3..467dbea44 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:16-alpine +FROM node:18.18-alpine # Set environment variables # Environment Variable to signal that we are running production diff --git a/README.md b/README.md index c0fd400e2..3a92c4572 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![Ontime build v2](https://github.com/cpvalente/ontime/actions/workflows/build_v2.yml/badge.svg)](https://github.com/cpvalente/ontime/actions/workflows/build_v2.yml) -[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-green.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Documentation in Gitbook](https://badges.aleen42.com/src/gitbook_2.svg)](https://ontime.gitbook.io) +[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-green.svg)](https://www.gnu.org/licenses/gpl-3.0) ## Download the latest releases here @@ -17,13 +17,14 @@ Ontime is an application for creating and managing event running order and timer The user inputs a list of events along with scheduling and event information. This will then populate a series of screens which are available to be rendered by any device in the Network. -This makes for a simple and cheap way to distribute over a venue using a network infrastructure instead of video -outputs. +This makes for a simple and cheap way to distribute over a venue using a network infrastructure instead of video outputs. ![App Window](https://github.com/cpvalente/ontime/blob/master/.github/aux-images/app.png) ![Views](https://github.com/cpvalente/ontime/blob/master/.github/aux-images/overview.png) +[Read the docs to learn more](https://docs.getontime.no) + ## Using Ontime Once installed and running, Ontime starts a background server that is the heart of all processes. @@ -37,7 +38,7 @@ You can then use the menu in the top left corner to select the desired view. The menu will be initially hidden until there is mouse interaction. In the case of unattended machines or automation, it is possible to use different URL to recall -individual views and extend view settings using the URL aliases feature +individual views and extend view settings using the URL presets feature ``` For the presentation views @@ -59,7 +60,7 @@ IP.ADDRESS:4001/editor > the control interface, same as the app IP.ADDRESS:4001/cuesheet > realtime cuesheets for collaboration ``` -More documentation is available [in our docs](https://ontime.gitbook.io) +More documentation is available [in our docs](https://docs.getontime.no) ## Feature List (in no specific order) @@ -84,7 +85,7 @@ More documentation is available [in our docs](https://ontime.gitbook.io) - WebSockets - [x] Roll mode: run standalone using the system clock - [x] [Headless run](#headless-run): run server in a separate machine, configure from a browser locally -- [x] [Countdown to anything!](https://ontime.gitbook.io/v2/views/countdown): have +- [x] [Countdown to anything!](https://docs.getontime.no/features/count-to-anything/): have a countdown to any scheduled event - [x] Multi-platform (available on Windows, MacOS and Linux) - [x] [Companion integration](https://bitfocus.io/connections/getontime-ontime) @@ -120,9 +121,14 @@ Ontime broadcasts its data over WebSockets. This allows you to consume its data Writing a new view for the browser can be done with basic knowledge of HTML + CSS + Javascript (or any other language that can run in the browser).
-See [this repository](https://github.com/cpvalente/ontime-viewer-template-v2) with a small template on -how to get you started and read the docs about -the [Websocket API](https://ontime.gitbook.io/v2/control-and-feedback/ontime-apis#osc-and-websocket-api) +We have prepared a few resources to help here: +- Shipped with Ontime there is a small clock to get you started, it is available at `http://localhost:4001/external/demo` and the [code can be found here](https://github.com/cpvalente/ontime/tree/master/apps/server/src/external/demo) +- See [this repository](https://github.com/cpvalente/ontime-viewer-template-v2) with a template on +how to get you started +- See information about the [Websocket API](https://docs.getontime.no/api/osc-and-ws/) +
+More information [in the docs](https://docs.getontime.no/features/custom-views/) + ### Headless run️ @@ -130,7 +136,7 @@ You can self-host and run Ontime in a docker image. The docker image along with documentation is [available Docker Hub at getontime/ontime](https://hub.docker.com/r/getontime/ontime) -If you want to run this image in a Raspberry Pi, please see [the docs](https://ontime.gitbook.io/v2/additional-notes/use-in-raspberry-pi) +If you want to run this image in a Raspberry Pi, please see [the docs](https://docs.getontime.no/additional-notes/use-with-rpi/) ## Roadmap @@ -192,7 +198,7 @@ Information about the project setup can be found in the [development documentati # Help -Help is underway! ... and can be found [here](https://ontime.gitbook.io) +Help is underway! ... and can be found [here](https://docs.getontime.no) # License diff --git a/apps/client/package.json b/apps/client/package.json index 0f8670d8a..97f9f3cdb 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -1,36 +1,36 @@ { "name": "ontime-ui", - "version": "2.28.17", + "version": "3.0.0-alpha", "private": true, + "type": "module", "dependencies": { "@chakra-ui/react": "^2.7.0", - "@dnd-kit/core": "^6.0.8", - "@dnd-kit/sortable": "^7.0.2", - "@dnd-kit/utilities": "^3.2.1", + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/sortable": "^8.0.0", + "@dnd-kit/utilities": "^3.2.2", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", + "@mantine/hooks": "^7.6.2", "@react-icons/all-files": "^4.1.0", - "@sentry/react": "^7.46.0", - "@sentry/tracing": "^7.46.0", - "@tanstack/react-query": "^5.8.4", - "@tanstack/react-query-devtools": "^5.8.4", - "@tanstack/react-table": "^8.9.2", + "@sentry/react": "^7.92.0", + "@tanstack/react-query": "^5.17.9", + "@tanstack/react-query-devtools": "^5.17.9", + "@tanstack/react-table": "^8.11.3", "autosize": "^6.0.1", "axios": "^1.2.0", "color": "^4.2.3", - "csv-stringify": "^6.2.3", - "deepmerge": "^4.3.0", + "csv-stringify": "^6.4.5", "framer-motion": "^10.10.0", "react": "^18.2.0", "react-colorful": "^5.6.1", "react-dom": "^18.2.0", - "react-fast-compare": "^3.2.0", - "react-hook-form": "^7.43.5", - "react-qr-code": "^2.0.11", + "react-fast-compare": "^3.2.2", + "react-hook-form": "^7.49.2", + "react-qr-code": "^2.0.12", "react-router-dom": "^6.3.0", "typeface-open-sans": "^1.1.13", "web-vitals": "^3.1.1", - "zustand": "^4.3.6" + "zustand": "^4.5.0" }, "scripts": { "addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js", @@ -45,7 +45,8 @@ "lint-staged": "eslint", "test": "vitest", "test:pipeline": "vitest run", - "cleanup": "rm -rf .turbo && rm -rf node_modules && rm -rf build" + "cleanup": "rm -rf .turbo && rm -rf node_modules && rm -rf build", + "analyse": "npx vite-bundle-visualizer" }, "browserslist": { "production": [ @@ -60,7 +61,7 @@ ] }, "devDependencies": { - "@sentry/vite-plugin": "^0.4.0", + "@sentry/vite-plugin": "^2.14.0", "@tanstack/eslint-plugin-query": "^5.8.4", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.1.1", @@ -71,7 +72,7 @@ "@types/testing-library__jest-dom": "^5.14.5", "@typescript-eslint/eslint-plugin": "^6.10.0", "@typescript-eslint/parser": "^6.10.0", - "@vitejs/plugin-react": "^3.0.1", + "@vitejs/plugin-react": "^4.2.1", "eslint": "^8.53.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-jest": "^27.6.0", @@ -85,11 +86,11 @@ "ontime-utils": "workspace:*", "prettier": "^3.0.3", "sass": "^1.57.1", - "typescript": "^5.2.2", - "vite": "^4.3.1", - "vite-plugin-compression2": "^0.9.0", - "vite-plugin-svgr": "^2.4.0", - "vite-tsconfig-paths": "^4.2.0", - "vitest": "^0.30.1" + "typescript": "^5.4.3", + "vite": "^5.1.0", + "vite-plugin-compression2": "^0.12.0", + "vite-plugin-svgr": "^4.2.0", + "vite-tsconfig-paths": "^4.3.1", + "vitest": "^1.2.2" } } diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx index 7cedda359..e72d6f4ff 100644 --- a/apps/client/src/App.tsx +++ b/apps/client/src/App.tsx @@ -47,9 +47,8 @@ function App() { } }; }, [isElectron, sendToElectron]); - return ( - + diff --git a/apps/client/src/AppRouter.tsx b/apps/client/src/AppRouter.tsx index 4c6f5726d..2f34d2acf 100644 --- a/apps/client/src/AppRouter.tsx +++ b/apps/client/src/AppRouter.tsx @@ -1,12 +1,13 @@ import { lazy, Suspense } from 'react'; import { Navigate, Route, Routes } from 'react-router-dom'; -import withAlias from './features/AliasWrapper'; +import Log from './features/log/Log'; +import withPreset from './features/PresetWrapper'; import withData from './features/viewers/ViewWrapper'; const Editor = lazy(() => import('./features/editors/ProtectedEditor')); const Cuesheet = lazy(() => import('./features/cuesheet/ProtectedCuesheet')); -const Operator = lazy(() => import('./features/operator/Operator')); +const Operator = lazy(() => import('./features/operator/OperatorExport')); const TimerView = lazy(() => import('./features/viewers/timer/Timer')); const MinimalTimerView = lazy(() => import('./features/viewers/minimal-timer/MinimalTimer')); @@ -15,58 +16,49 @@ const Countdown = lazy(() => import('./features/viewers/countdown/Countdown')); const Backstage = lazy(() => import('./features/viewers/backstage/Backstage')); const Public = lazy(() => import('./features/viewers/public/Public')); -const Lower = lazy(() => import('./features/viewers/lower-thirds/LowerWrapper')); +const Lower = lazy(() => import('./features/viewers/lower-thirds/LowerThird')); const StudioClock = lazy(() => import('./features/viewers/studio/StudioClock')); -const STimer = withAlias(withData(TimerView)); -const SMinimalTimer = withAlias(withData(MinimalTimerView)); -const SClock = withAlias(withData(ClockView)); -const SCountdown = withAlias(withData(Countdown)); -const SBackstage = withAlias(withData(Backstage)); -const SPublic = withAlias(withData(Public)); -const SLowerThird = withAlias(withData(Lower)); -const SStudio = withAlias(withData(StudioClock)); +const STimer = withPreset(withData(TimerView)); +const SMinimalTimer = withPreset(withData(MinimalTimerView)); +const SClock = withPreset(withData(ClockView)); +const SCountdown = withPreset(withData(Countdown)); +const SBackstage = withPreset(withData(Backstage)); +const SPublic = withPreset(withData(Public)); +const SLowerThird = withPreset(withData(Lower)); +const SStudio = withPreset(withData(StudioClock)); const EditorFeatureWrapper = lazy(() => import('./features/EditorFeatureWrapper')); const RundownPanel = lazy(() => import('./features/rundown/RundownExport')); const TimerControl = lazy(() => import('./features/control/playback/TimerControlExport')); const MessageControl = lazy(() => import('./features/control/message/MessageControlExport')); -const Info = lazy(() => import('./features/info/InfoExport')); export default function AppRouter() { return ( } /> - } /> - } /> - } /> } /> } /> - } /> - } /> } /> } /> - } /> } /> } /> + } /> - {/*/!* Lower cannot have fallback *!/*/} + } /> } /> - } /> {/*/!* Protected Routes *!/*/} } /> } /> - } /> - } /> {/*/!* Protected Routes - Elements *!/*/} - + } /> diff --git a/apps/client/src/assets/fonts/DSEG7ClassicMini-Regular.ttf b/apps/client/src/assets/fonts/DSEG7ClassicMini-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ffe3dd7da912f34bf87999b80a42a8fa3f49265e GIT binary patch literal 23116 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 { + const res = await axios.get(customFieldsPath); + return res.data; +} + +/** + * Sets list of known custom fields + */ +export async function postCustomField(newField: CustomField): Promise { + const res = await axios.post(customFieldsPath, { ...newField }); + return res.data; +} + +/** + * Edits single custom field + */ +export async function editCustomField(label: CustomFieldLabel, newField: CustomField): Promise { + const res = await axios.put(`${customFieldsPath}/${label}`, { ...newField }); + return res.data; +} + +/** + * Deletes single custom field + */ +export async function deleteCustomField(label: CustomFieldLabel): Promise { + const res = await axios.delete(`${customFieldsPath}/${label}`); + return res.data; +} diff --git a/apps/client/src/common/api/db.ts b/apps/client/src/common/api/db.ts new file mode 100644 index 000000000..27a3b5e50 --- /dev/null +++ b/apps/client/src/common/api/db.ts @@ -0,0 +1,168 @@ +import axios, { AxiosResponse } from 'axios'; +import { DatabaseModel, GetInfo, MessageResponse, ProjectData, ProjectFileListResponse } from 'ontime-types'; + +import { makeCSV, makeTable } from '../../features/cuesheet/cuesheetUtils'; + +import { apiEntryUrl } from './constants'; +import { createBlob, downloadBlob } from './utils'; + +const dbPath = `${apiEntryUrl}/db`; + +/** + * HTTP request to the current DB + */ +async function getDb(): Promise> { + return axios.get(`${dbPath}/download`); +} + +/** + * Request download of the current project file + * @param fileName + */ +export async function downloadProject(fileName: string = 'ontime-project') { + try { + const { data, name } = await fileDownload(fileName); + + const fileContent = JSON.stringify(data, null, 2); + + const blob = createBlob(fileContent, 'application/json;charset=utf-8;'); + downloadBlob(blob, `${name}.json`); + } catch (error) { + console.error(error); + } +} + +/** + * Request download of the current rundown as a CSV file + * @param fileName + */ +export async function downloadCSV(fileName: string = 'rundown') { + try { + const { data, name } = await fileDownload(fileName); + const { project, rundown, customFields } = data; + + const sheetData = makeTable(project, rundown, customFields); + const fileContent = makeCSV(sheetData); + + const blob = createBlob(fileContent, 'text/csv;charset=utf-8;'); + downloadBlob(blob, `${name}.csv`); + } catch (error) { + console.error(error); + } +} + +/** + * HTTP request to upload project file + */ +export async function uploadProjectFile(file: File): Promise { + const formData = new FormData(); + formData.append('project', file); + const response = await axios.post(`${dbPath}/upload`, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + return response.data; +} + +/** + * Make patch changes to the objects in the db + */ +export async function patchData(patchDb: Partial): Promise> { + return await axios.patch(dbPath, patchDb); +} + +/** + * HTTP request to create a project file + */ +export async function createProject( + project: Partial< + ProjectData & { + filename: string; + } + >, +): Promise { + const res = await axios.post(`${dbPath}/new`, project); + return res.data; +} + +/** + * HTTP request to get the list of available project files + */ +export async function getProjects(): Promise { + const res = await axios.get(`${dbPath}/all`); + return res.data; +} + +/** + * HTTP request to load a project file + */ +export async function loadProject(filename: string): Promise { + const res = await axios.post(`${dbPath}/load`, { + filename, + }); + return res.data; +} + +/** + * HTTP request to duplicate a project file + */ +export async function duplicateProject(filename: string, newFilename: string): Promise { + const url = `${dbPath}/${filename}/duplicate`; + const decodedUrl = decodeURIComponent(url); + const res = await axios.post(decodedUrl, { + newFilename, + }); + return res.data; +} + +/** + * HTTP request to rename a project file + */ +export async function renameProject(filename: string, newFilename: string): Promise { + const url = `${dbPath}/${filename}/rename`; + const decodedUrl = decodeURIComponent(url); + const res = await axios.put(decodedUrl, { + newFilename, + }); + return res.data; +} + +/** + * HTTP request to delete a project file + */ +export async function deleteProject(filename: string): Promise { + const url = `${dbPath}/${filename}`; + const decodedUrl = decodeURIComponent(url); + const res = await axios.delete(decodedUrl); + return res.data; +} + +/** + * HTTP request to retrieve application info + */ +export async function getInfo(): Promise { + const res = await axios.get(`${dbPath}/info`); + return res.data; +} + +/** + * Utility function gets project from db + * @param fileName + * @returns + */ +async function fileDownload(fileName: string): Promise<{ data: DatabaseModel; name: string }> { + const response = await getDb(); + + const headerLine = response.headers['Content-Disposition']; + + // try and get the filename from the response + let name = fileName; + if (headerLine != null) { + const startFileNameIndex = headerLine.indexOf('"') + 1; + const endFileNameIndex = headerLine.lastIndexOf('"'); + name = headerLine.substring(startFileNameIndex, endFileNameIndex); + } + + return { data: response.data, name }; +} diff --git a/apps/client/src/common/api/eventsApi.ts b/apps/client/src/common/api/eventsApi.ts deleted file mode 100644 index 8196853e2..000000000 --- a/apps/client/src/common/api/eventsApi.ts +++ /dev/null @@ -1,90 +0,0 @@ -import axios from 'axios'; -import { GetRundownCached, OntimeRundown, OntimeRundownEntry } from 'ontime-types'; - -import { rundownURL } from './apiConstants'; - -/** - * @description HTTP request to fetch all events - * @return {Promise} - */ -export async function fetchCachedRundown(): Promise { - const res = await axios.get(`${rundownURL}/cached`); - return res.data; -} - -/** - * @deprecated use fetchCachedRundown instead - * @description HTTP request to fetch all events - * @return {Promise} - */ -export async function fetchRundown(): Promise { - const res = await axios.get(rundownURL); - return res.data; -} - -/** - * @description HTTP request to post new event - * @return {Promise} - */ -export async function requestPostEvent(data: OntimeRundownEntry) { - return axios.post(rundownURL, data); -} - -/** - * @description HTTP request to put new event - * @return {Promise} - */ -export async function requestPutEvent(data: Partial) { - return axios.put(rundownURL, data); -} - -export type ReorderEntry = { - eventId: string; - from: number; - to: number; -}; - -/** - * @description HTTP request to reorder events - * @return {Promise} - */ -export async function requestReorderEvent(data: ReorderEntry) { - return axios.patch(`${rundownURL}/reorder`, data); -} - -/** - * @description HTTP request to request application of delay - * @return {Promise} - */ -export async function requestApplyDelay(eventId: string) { - return axios.patch(`${rundownURL}/applydelay/${eventId}`); -} - -export type SwapEntry = { - from: string; - to: string; -}; - -/** - * @description HTTP request to swap two events - * @return {Promise} - */ -export async function requestEventSwap(data: SwapEntry) { - return axios.patch(`${rundownURL}/swap`, data); -} - -/** - * @description HTTP request to delete given event - * @return {Promise} - */ -export async function requestDelete(eventId: string) { - return axios.delete(`${rundownURL}/${eventId}`); -} - -/** - * @description HTTP request to delete all events - * @return {Promise} - */ -export async function requestDeleteAll() { - return axios.delete(`${rundownURL}/all`); -} diff --git a/apps/client/src/common/api/excel.ts b/apps/client/src/common/api/excel.ts new file mode 100644 index 000000000..b73aa585f --- /dev/null +++ b/apps/client/src/common/api/excel.ts @@ -0,0 +1,42 @@ +import axios, { AxiosResponse } from 'axios'; +import { CustomFields, OntimeRundown } from 'ontime-types'; +import { ImportMap } from 'ontime-utils'; + +import { apiEntryUrl } from './constants'; + +const excelPath = `${apiEntryUrl}/excel`; + +type PreviewSpreadsheetResponse = { + rundown: OntimeRundown; + customFields: CustomFields; +}; + +/** + * upload Excel file to server + * @return string - file ID op the uploaded file + */ +export async function upload(file: File) { + const formData = new FormData(); + formData.append('excel', file); + await axios.post(`${excelPath}/upload`, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); +} + +/** + * Get Worksheet names + * @return string[] - array of available worksheets + */ +export async function getWorksheetNames(): Promise { + const response: AxiosResponse = await axios.get(`${excelPath}/worksheets`); + return response.data; +} + +export async function importRundownPreview(options: ImportMap): Promise { + const response: AxiosResponse = await axios.post(`${excelPath}/preview`, { + options, + }); + return response.data; +} diff --git a/apps/client/src/common/api/external.ts b/apps/client/src/common/api/external.ts new file mode 100644 index 000000000..e84e49a15 --- /dev/null +++ b/apps/client/src/common/api/external.ts @@ -0,0 +1,19 @@ +import axios from 'axios'; + +import { apiRepoLatest } from '../../externals'; + +export type HasUpdate = { + url: string; + version: string; +}; + +/** + * HTTP request to get the latest version and url from github + */ +export async function getLatestVersion(): Promise { + const res = await axios.get(`${apiRepoLatest}`); + return { + url: res.data.html_url as string, + version: res.data.tag_name as string, + }; +} diff --git a/apps/client/src/common/api/http.ts b/apps/client/src/common/api/http.ts new file mode 100644 index 000000000..e479e3c4f --- /dev/null +++ b/apps/client/src/common/api/http.ts @@ -0,0 +1,21 @@ +import axios, { AxiosResponse } from 'axios'; +import { HttpSettings } from 'ontime-types'; + +import { apiEntryUrl } from './constants'; + +const httpPath = `${apiEntryUrl}/http`; + +/** + * HTTP request to retrieve http settings + */ +export async function getHTTP(): Promise { + const res = await axios.get(httpPath); + return res.data; +} + +/** + * HTTP request to mutate http settings + */ +export async function postHTTP(data: HttpSettings): Promise> { + return axios.post(httpPath, data); +} diff --git a/apps/client/src/common/api/ontimeApi.ts b/apps/client/src/common/api/ontimeApi.ts deleted file mode 100644 index 2d2d20696..000000000 --- a/apps/client/src/common/api/ontimeApi.ts +++ /dev/null @@ -1,320 +0,0 @@ -import axios, { AxiosResponse } from 'axios'; -import { - Alias, - DatabaseModel, - GetInfo, - HttpSettings, - OntimeRundown, - OSCSettings, - OscSubscription, - ProjectData, - Settings, - UserFields, - ViewSettings, -} from 'ontime-types'; -import { ExcelImportMap } from 'ontime-utils'; - -import { apiRepoLatest } from '../../externals'; -import fileDownload from '../utils/fileDownload'; - -import { ontimeURL } from './apiConstants'; - -/** - * @description HTTP request to retrieve application settings - * @return {Promise} - */ -export async function getSettings(): Promise { - const res = await axios.get(`${ontimeURL}/settings`); - return res.data; -} - -/** - * @description HTTP request to mutate application settings - * @return {Promise} - */ -export async function postSettings(data: Settings) { - return axios.post(`${ontimeURL}/settings`, data); -} - -/** - * @description HTTP request to retrieve application info - * @return {Promise} - */ -export async function getInfo(): Promise { - const res = await axios.get(`${ontimeURL}/info`); - return res.data; -} - -/** - * @description HTTP request to retrieve view settings - * @return {Promise} - */ -export async function getView(): Promise { - const res = await axios.get(`${ontimeURL}/views`); - return res.data; -} - -/** - * @description HTTP request to mutate view settings - * @return {Promise} - */ -export async function postViewSettings(data: ViewSettings) { - return axios.post(`${ontimeURL}/views`, data); -} - -/** - * @description HTTP request to retrieve aliases - * @return {Promise} - */ -export async function getAliases(): Promise { - const res = await axios.get(`${ontimeURL}/aliases`); - return res.data; -} - -/** - * @description HTTP request to mutate aliases - * @return {Promise} - */ -export async function postAliases(data: Alias[]) { - return axios.post(`${ontimeURL}/aliases`, data); -} - -/** - * @description HTTP request to retrieve user fields - * @return {Promise} - */ -export async function getUserFields(): Promise { - const res = await axios.get(`${ontimeURL}/userfields`); - return res.data; -} - -/** - * @description HTTP request to mutate user fields - * @return {Promise} - */ -export async function postUserFields(data: UserFields) { - return axios.post(`${ontimeURL}/userfields`, data); -} - -/** - * @description HTTP request to retrieve osc settings - * @return {Promise} - */ -export async function getOSC(): Promise { - const res = await axios.get(`${ontimeURL}/osc`); - return res.data; -} - -/** - * @description HTTP request to retrieve http settings - * @return {Promise} - */ -export async function getHTTP(): Promise { - const res = await axios.get(`${ontimeURL}/http`); - return res.data; -} - -/** - * @description HTTP request to mutate http settings - * @return {Promise} - */ -export async function postHTTP(data: HttpSettings) { - return axios.post(`${ontimeURL}/http`, data); -} - -/** - * @description HTTP request to mutate osc settings - * @return {Promise} - */ -export async function postOSC(data: OSCSettings) { - return axios.post(`${ontimeURL}/osc`, data); -} - -/** - * @description HTTP request to mutate osc subscriptions - * @return {Promise} - */ -export async function postOscSubscriptions(data: OscSubscription) { - return axios.post(`${ontimeURL}/osc-subscriptions`, data); -} - -/** - * @description HTTP request to download db in CSV format - */ -export const downloadCSV = () => { - return fileDownload(ontimeURL, { name: 'rundown', type: 'csv' }, { type: 'text/csv;charset=utf-8;' }); -}; - -/** - * @description HTTP request to download db in JSON format - */ -export const downloadRundown = () => { - return fileDownload(ontimeURL, { name: 'rundown', type: 'json' }, { type: 'application/json;charset=utf-8;' }); -}; - -// TODO: should this be extracted to shared code? -export type ProjectFileImportOptions = { - onlyRundown: boolean; -}; - -/** - * @description HTTP request to upload events db - * @return {Promise} - */ -export const uploadProjectFile = async ( - file: File, - setProgress: (value: number) => void, - options?: Partial, -) => { - const formData = new FormData(); - formData.append('userFile', file); - - const onlyRundown = Boolean(options?.onlyRundown); - - await axios - .post(`${ontimeURL}/db?onlyRundown=${onlyRundown}`, formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - onUploadProgress: (progressEvent) => { - const complete = progressEvent?.total ? Math.round((progressEvent.loaded * 100) / progressEvent.total) : 0; - setProgress(complete); - }, - }) - .then((response) => response.data.id); -}; - -/** - * @description Make patch changes to the objects in the db - * @return {Promise} - */ -export async function patchData(patchDb: Partial) { - const response = await axios.patch(`${ontimeURL}/db`, patchDb); - return response; -} - -type PostPreviewExcelResponse = { - rundown: OntimeRundown; - project: ProjectData; - userFields: UserFields; -}; - -/** - * @description Make patch changes to the objects in the db - * @return {Promise} - returns parsed rundown and userfields - */ -export async function postPreviewExcel(file: File, setProgress: (value: number) => void, options?: ExcelImportMap) { - const formData = new FormData(); - formData.append('userFile', file); - formData.append('options', JSON.stringify(options)); - - const response: AxiosResponse = await axios.post( - `${ontimeURL}/preview-spreadsheet`, - formData, - { - headers: { - 'Content-Type': 'multipart/form-data', - }, - onUploadProgress: (progressEvent) => { - const complete = progressEvent?.total ? Math.round((progressEvent.loaded * 100) / progressEvent.total) : 0; - setProgress(complete); - }, - }, - ); - - return response; -} - -export type HasUpdate = { - url: string; - version: string; -}; - -/** - * @description HTTP request to get the latest version and url from github - * @return {Promise} - */ -export async function getLatestVersion(): Promise { - const res = await axios.get(`${apiRepoLatest}`); - return { - url: res.data.html_url as string, - version: res.data.tag_name as string, - }; -} - -export async function postNew(initialData: Partial) { - return axios.post(`${ontimeURL}/new`, initialData); -} - -/** - * @description STEP 1 - */ -export const uploadSheetClientFile = async (file: File) => { - const formData = new FormData(); - formData.append('userFile', file); - const res = await axios - .post(`${ontimeURL}/sheet/clientsecret`, formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }) - .then((response) => response.data.id); - return res; -}; - -/** - * @description STEP 1 test - */ -export const getClientSecrect = async () => { - const response = await axios.get(`${ontimeURL}/sheet/clientsecret`); - return response.data; -}; - -/** - * @description STEP 2 - */ -export const getSheetsAuthUrl = async () => { - const response = await axios.get(`${ontimeURL}/sheet/authentication/url`); - return response.data; -}; - -/** - * @description STEP 2 test - */ -export const getAuthentication = async () => { - const response = await axios.get(`${ontimeURL}/sheet/authentication`); - return response.data; -}; - -/** - * @description STEP 3 - * @returns worksheetOptions - */ -export const postId = async (id: string) => { - const response = await axios.post(`${ontimeURL}/sheet/id`, { id }); - return response.data; -}; - -/** - * @description STEP 4 - */ -export const postWorksheet = async (id: string, worksheet: string) => { - const response = await axios.post(`${ontimeURL}/sheet/worksheet`, { id, worksheet }); - return response.data; -}; - -/** - * @description STEP 5 - */ -export const postPreviewSheet = async (id: string, options: ExcelImportMap) => { - const response = await axios.post(`${ontimeURL}/sheet/pull`, { id, options }); - return response.data.data; -}; - -/** - * @description STEP 5 - */ -export const postPushSheet = async (id: string, options: ExcelImportMap) => { - const response = await axios.post(`${ontimeURL}/sheet-push`, { id, options }); - return response.data.data; -}; diff --git a/apps/client/src/common/api/osc.ts b/apps/client/src/common/api/osc.ts new file mode 100644 index 000000000..e3f0aa834 --- /dev/null +++ b/apps/client/src/common/api/osc.ts @@ -0,0 +1,21 @@ +import axios, { AxiosResponse } from 'axios'; +import { OSCSettings } from 'ontime-types'; + +import { apiEntryUrl } from './constants'; + +const oscPath = `${apiEntryUrl}/osc`; + +/** + * HTTP request to retrieve osc settings + */ +export async function getOSC(): Promise { + const res = await axios.get(oscPath); + return res.data; +} + +/** + * HTTP request to mutate osc settings + */ +export async function postOSC(data: OSCSettings): Promise> { + return axios.post(oscPath, data); +} diff --git a/apps/client/src/common/api/project.ts b/apps/client/src/common/api/project.ts new file mode 100644 index 000000000..11b14d267 --- /dev/null +++ b/apps/client/src/common/api/project.ts @@ -0,0 +1,21 @@ +import axios, { AxiosResponse } from 'axios'; +import { ProjectData } from 'ontime-types'; + +import { apiEntryUrl } from './constants'; + +const projectPath = `${apiEntryUrl}/project`; + +/** + * HTTP request to fetch project data + */ +export async function getProjectData(): Promise { + const res = await axios.get(projectPath); + return res.data; +} + +/** + * HTTP request to mutate project data + */ +export async function postProjectData(data: ProjectData): Promise> { + return axios.post(projectPath, data); +} diff --git a/apps/client/src/common/api/projectDataApi.ts b/apps/client/src/common/api/projectDataApi.ts deleted file mode 100644 index b89bebffb..000000000 --- a/apps/client/src/common/api/projectDataApi.ts +++ /dev/null @@ -1,21 +0,0 @@ -import axios from 'axios'; -import { ProjectData } from 'ontime-types'; - -import { projectDataURL } from './apiConstants'; - -/** - * @description HTTP request to fetch project data - * @return {Promise} - */ -export async function getProjectData(): Promise { - const res = await axios.get(projectDataURL); - return res.data; -} - -/** - * @description HTTP request to mutate project data - * @return {Promise} - */ -export async function postProjectData(data: ProjectData) { - return axios.post(projectDataURL, data); -} diff --git a/apps/client/src/common/api/rundown.ts b/apps/client/src/common/api/rundown.ts new file mode 100644 index 000000000..2849f274d --- /dev/null +++ b/apps/client/src/common/api/rundown.ts @@ -0,0 +1,86 @@ +import axios, { AxiosResponse } from 'axios'; +import { MessageResponse, OntimeEvent, OntimeRundownEntry, RundownCached } from 'ontime-types'; + +import { apiEntryUrl } from './constants'; + +const rundownPath = `${apiEntryUrl}/rundown`; + +/** + * HTTP request to fetch all events + */ +export async function fetchNormalisedRundown(): Promise { + const res = await axios.get(`${rundownPath}/normalised`); + return res.data; +} + +/** + * HTTP request to post new event + */ +export async function requestPostEvent(data: Partial): Promise> { + return axios.post(rundownPath, data); +} + +/** + * HTTP request to put new event + */ +export async function requestPutEvent(data: Partial): Promise> { + return axios.put(rundownPath, data); +} + +type BatchEditEntry = { + data: Partial; + ids: string[]; +}; + +/** + * HTTP request to put multiple events + */ +export async function requestBatchPutEvents(data: BatchEditEntry): Promise> { + return axios.put(`${rundownPath}/batch`, data); +} + +export type ReorderEntry = { + eventId: string; + from: number; + to: number; +}; + +/** + * HTTP request to reorder events + */ +export async function requestReorderEvent(data: ReorderEntry): Promise> { + return axios.patch(`${rundownPath}/reorder`, data); +} + +export type SwapEntry = { + from: string; + to: string; +}; + +/** + * HTTP request to swap two events + */ +export async function requestEventSwap(data: SwapEntry): Promise> { + return axios.patch(`${rundownPath}/swap`, data); +} + +/** + * HTTP request to request application of delay + */ +export async function requestApplyDelay(eventId: string): Promise> { + return axios.patch(`${rundownPath}/applydelay/${eventId}`); +} + +/** + * HTTP request to delete given event + */ +export async function requestDelete(eventId: string): Promise> { + return axios.delete(`${rundownPath}/${eventId}`); +} + +/** + * HTTP request to delete all events + */ +export async function requestDeleteAll(): Promise> { + return axios.delete(`${rundownPath}/all`); +} diff --git a/apps/client/src/common/api/settings.ts b/apps/client/src/common/api/settings.ts new file mode 100644 index 000000000..a4a9aa1c2 --- /dev/null +++ b/apps/client/src/common/api/settings.ts @@ -0,0 +1,21 @@ +import axios, { AxiosResponse } from 'axios'; +import { Settings } from 'ontime-types'; + +import { apiEntryUrl } from './constants'; + +const settingsPath = `${apiEntryUrl}/settings`; + +/** + * HTTP request to retrieve application settings + */ +export async function getSettings(): Promise { + const res = await axios.get(settingsPath); + return res.data; +} + +/** + * HTTP request to mutate application settings + */ +export async function postSettings(data: Settings): Promise> { + return axios.post(settingsPath, data); +} diff --git a/apps/client/src/common/api/sheets.ts b/apps/client/src/common/api/sheets.ts new file mode 100644 index 000000000..ebeb9fdce --- /dev/null +++ b/apps/client/src/common/api/sheets.ts @@ -0,0 +1,75 @@ +import axios, { AxiosResponse } from 'axios'; +import { AuthenticationStatus, CustomFields, OntimeRundown } from 'ontime-types'; +import { ImportMap } from 'ontime-utils'; + +import { apiEntryUrl } from './constants'; + +const sheetsPath = `${apiEntryUrl}/sheets`; + +/** + * HTTP request to verify whether we are authenticated with Google Sheet service + */ +export const verifyAuthenticationStatus = async (): Promise<{ + authenticated: AuthenticationStatus; + sheetId: string; +}> => { + const response = await axios.get(`${sheetsPath}/connect`); + return response.data; +}; + +/** + * HTTP request to initiate the authentication service with google + */ +export const requestConnection = async ( + file: File, + sheetId: string, +): Promise<{ + verification_url: string; + user_code: string; +}> => { + const formData = new FormData(); + formData.append('client_secret', file); + + const response = await axios.post(`${sheetsPath}/${sheetId}/connect`, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + return response.data; +}; + +/** + * HTTP request to revoke authentication to google sheet + */ +export const revokeAuthentication = async (): Promise<{ authenticated: AuthenticationStatus }> => { + const response = await axios.post(`${sheetsPath}/revoke`); + return response.data; +}; + +/** + * HTTP request to upload preview the contents of a google sheet as rundown + */ +export const previewRundown = async ( + sheetId: string, + options: ImportMap, +): Promise<{ + rundown: OntimeRundown; + customFields: CustomFields; +}> => { + const response = await axios.post(`${sheetsPath}/${sheetId}/read`, { options }); + return response.data; +}; + +export const getWorksheetNames = async (sheetId: string): Promise => { + const response: AxiosResponse = await axios.post(`${sheetsPath}/${sheetId}/worksheets`); + return response.data; +}; + +/** + * HTTP request to upload the rundown to a google sheet + */ +export const uploadRundown = async (sheetId: string, options: ImportMap): Promise => { + const response = await axios.post(`${sheetsPath}/${sheetId}/write`, { options }); + return response.data; +}; diff --git a/apps/client/src/common/api/urlPresets.ts b/apps/client/src/common/api/urlPresets.ts new file mode 100644 index 000000000..68820a155 --- /dev/null +++ b/apps/client/src/common/api/urlPresets.ts @@ -0,0 +1,21 @@ +import axios from 'axios'; +import { URLPreset } from 'ontime-types'; + +import { apiEntryUrl } from './constants'; + +const urlPresetsPath = `${apiEntryUrl}/url-presets`; + +/** + * HTTP request to retrieve aliases + */ +export async function getUrlPresets(): Promise { + const res = await axios.get(urlPresetsPath); + return res.data; +} + +/** + * HTTP request to mutate aliases + */ +export async function postUrlPresets(data: URLPreset[]): Promise { + return axios.post(urlPresetsPath, data); +} diff --git a/apps/client/src/common/api/apiUtils.ts b/apps/client/src/common/api/utils.ts similarity index 57% rename from apps/client/src/common/api/apiUtils.ts rename to apps/client/src/common/api/utils.ts index 29f919212..cc9f7a79b 100644 --- a/apps/client/src/common/api/apiUtils.ts +++ b/apps/client/src/common/api/utils.ts @@ -6,6 +6,11 @@ import { ontimeQueryClient } from '../queryClient'; import { addLog } from '../stores/logger'; import { nowInMillis } from '../utils/time'; +/** + * Utility unrwap a potential axios error + * @param error + * @returns + */ export function maybeAxiosError(error: unknown) { if (axios.isAxiosError(error)) { const statusText = (error as AxiosError).response?.statusText ?? ''; @@ -26,6 +31,11 @@ export function maybeAxiosError(error: unknown) { } } +/** + * Utility unrwaps a potential axios error and sends to logger + * @param prepend + * @param error + */ export function logAxiosError(prepend: string, error: unknown) { const message = `${prepend}: ${maybeAxiosError(error)}`; @@ -42,12 +52,32 @@ export function logAxiosError(prepend: string, error: unknown) { * Utility function invalidates react-query caches */ export async function invalidateAllCaches() { - await ontimeQueryClient.invalidateQueries({ queryKey: ['project'] }); - await ontimeQueryClient.invalidateQueries({ queryKey: ['aliases'] }); - await ontimeQueryClient.invalidateQueries({ queryKey: ['userFields'] }); - await ontimeQueryClient.invalidateQueries({ queryKey: ['rundown'] }); - await ontimeQueryClient.invalidateQueries({ queryKey: ['appinfo'] }); - await ontimeQueryClient.invalidateQueries({ queryKey: ['oscSettings'] }); - await ontimeQueryClient.invalidateQueries({ queryKey: ['appSettings'] }); - await ontimeQueryClient.invalidateQueries({ queryKey: ['viewSettings'] }); + await ontimeQueryClient.invalidateQueries(); +} + +/** + * Creates blob from content + * @param fileContent + * @param type + * @returns + */ +export function createBlob(fileContent: string, type: string): Blob { + return new Blob([fileContent], { type }); +} + +/** + * downloads a blob + * @param downloadUrl + * @param fileName + */ +export function downloadBlob(blob: Blob, fileName: string) { + const downloadUrl = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.setAttribute('href', downloadUrl); + link.setAttribute('download', fileName); + document.body.appendChild(link); + link.click(); + + // Clean up the URL.createObjectURL to release resources + URL.revokeObjectURL(downloadUrl); } diff --git a/apps/client/src/common/api/viewSettings.ts b/apps/client/src/common/api/viewSettings.ts new file mode 100644 index 000000000..f835f29e7 --- /dev/null +++ b/apps/client/src/common/api/viewSettings.ts @@ -0,0 +1,21 @@ +import axios from 'axios'; +import { ViewSettings } from 'ontime-types'; + +import { apiEntryUrl } from './constants'; + +const viewSettingsPath = `${apiEntryUrl}/view-settings`; + +/** + * HTTP request to retrieve view settings + */ +export async function getView(): Promise { + const res = await axios.get(viewSettingsPath); + return res.data; +} + +/** + * HTTP request to mutate view settings + */ +export async function postViewSettings(data: ViewSettings) { + return axios.post(viewSettingsPath, data); +} diff --git a/apps/client/src/common/components/app-link/AppLink.module.scss b/apps/client/src/common/components/app-link/AppLink.module.scss new file mode 100644 index 000000000..ce4b01bb7 --- /dev/null +++ b/apps/client/src/common/components/app-link/AppLink.module.scss @@ -0,0 +1,12 @@ +@use "../../../theme/mixins" as *; + +.link { + @include action-link; + font-size: $inner-section-text-size; +} + +.linkIcon { + margin-left: $element-inner-spacing; + display: inline-block; + transform: rotate(45deg); +} diff --git a/apps/client/src/common/components/app-link/AppLink.tsx b/apps/client/src/common/components/app-link/AppLink.tsx new file mode 100644 index 000000000..3137d0774 --- /dev/null +++ b/apps/client/src/common/components/app-link/AppLink.tsx @@ -0,0 +1,26 @@ +import { MouseEvent, ReactNode } from 'react'; +import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp'; + +import { openLink } from '../../utils/linkUtils'; + +import style from './AppLink.module.scss'; + +interface AppLinkProps { + href: string; + children: ReactNode; +} + +export default function AppLink(props: AppLinkProps) { + const { href, children } = props; + + const handleClick = (event: MouseEvent) => { + event.preventDefault(); + openLink(href); + }; + + return ( + + {children} + + ); +} diff --git a/apps/client/src/common/components/buttons/QuitIconBtn.tsx b/apps/client/src/common/components/buttons/QuitIconBtn.tsx index 43b03b85f..b99d24191 100644 --- a/apps/client/src/common/components/buttons/QuitIconBtn.tsx +++ b/apps/client/src/common/components/buttons/QuitIconBtn.tsx @@ -10,14 +10,12 @@ import { IconButton, Tooltip, } from '@chakra-ui/react'; -import { FiPower } from '@react-icons/all-files/fi/FiPower'; +import { IoPowerOutline } from '@react-icons/all-files/io5/IoPowerOutline'; -import { Size } from '../../models/Util.type'; import { useEmitLog } from '../../stores/logger'; interface QuitIconBtnProps { clickHandler: () => void; - size?: Size; disabled?: boolean; } @@ -42,7 +40,7 @@ const quitBtnStyle = { }; export default function QuitIconBtn(props: QuitIconBtnProps) { - const { clickHandler, size = 'lg', disabled, ...rest } = props; + const { clickHandler, disabled, ...rest } = props; const [isOpen, setIsOpen] = useState(false); const { emitInfo } = useEmitLog(); const onClose = () => setIsOpen(false); @@ -67,8 +65,8 @@ export default function QuitIconBtn(props: QuitIconBtnProps) { } + size='lg' + icon={} onClick={() => setIsOpen(true)} isDisabled={disabled} {...quitBtnStyle} @@ -81,7 +79,7 @@ export default function QuitIconBtn(props: QuitIconBtnProps) { Ontime Shutdown - This will shutdown the program and all running servers. Are you sure? + This will shutdown the Ontime server. Are you sure? - } variant='ontime-filled' tabIndex={-1} onClick={handleClick} /> + } + variant='ontime-filled' + tabIndex={-1} + onClick={handleClick} + isDisabled={disabled} + /> ); diff --git a/apps/client/src/common/components/delay-indicator/DelayIndicator.module.scss b/apps/client/src/common/components/delay-indicator/DelayIndicator.module.scss index 7f7ce3cb2..deb93d0a6 100644 --- a/apps/client/src/common/components/delay-indicator/DelayIndicator.module.scss +++ b/apps/client/src/common/components/delay-indicator/DelayIndicator.module.scss @@ -1,5 +1,3 @@ -@use '../../../theme/v2Styles' as *; - .delaySymbol { svg { font-size: 1.5rem; diff --git a/apps/client/src/common/components/error-boundary/ErrorBoundary.jsx b/apps/client/src/common/components/error-boundary/ErrorBoundary.jsx index dcd522a08..317c03d19 100644 --- a/apps/client/src/common/components/error-boundary/ErrorBoundary.jsx +++ b/apps/client/src/common/components/error-boundary/ErrorBoundary.jsx @@ -3,7 +3,7 @@ import React from 'react'; // skipcq: JS-C1003 - sentry does not expose itself as an ES Module. import * as Sentry from '@sentry/react'; -import { runtime } from '@/common/stores/runtime'; +import { runtimeStore } from '@/common/stores/runtime'; import { hasConnected, reconnectAttempts, shouldReconnect } from '@/common/utils/socket'; import style from './ErrorBoundary.module.scss'; @@ -23,13 +23,13 @@ class ErrorBoundary extends React.Component { componentDidCatch(error, info) { this.setState({ - error: error, + error, errorInfo: info, }); Sentry.withScope((scope) => { scope.setExtras('error', error); - scope.setExtras('store', runtime.getState()); + scope.setExtras('store', runtimeStore.getState()); scope.setExtras('hasSocket', { hasConnected, shouldReconnect, reconnectAttempts }); const eventId = Sentry.captureException(error); this.setState({ eventId, info }); diff --git a/apps/client/src/common/components/error-boundary/ErrorBoundary.module.scss b/apps/client/src/common/components/error-boundary/ErrorBoundary.module.scss index 71182baf6..eeb746c27 100644 --- a/apps/client/src/common/components/error-boundary/ErrorBoundary.module.scss +++ b/apps/client/src/common/components/error-boundary/ErrorBoundary.module.scss @@ -1,5 +1,3 @@ -@use '../../../theme/v2Styles' as *; - .errorContainer { width: 100%; height: 100%; diff --git a/apps/client/src/features/modals/ModalLink.module.scss b/apps/client/src/common/components/external-link/ExternalLink.module.scss similarity index 73% rename from apps/client/src/features/modals/ModalLink.module.scss rename to apps/client/src/common/components/external-link/ExternalLink.module.scss index 1da64e2a8..3aafa8c42 100644 --- a/apps/client/src/features/modals/ModalLink.module.scss +++ b/apps/client/src/common/components/external-link/ExternalLink.module.scss @@ -1,14 +1,12 @@ -@use '../../theme/v2Styles' as *; -@use '../../theme/ontimeColours' as *; - .link { display: flex; align-items: center; - gap: 4px; + gap: 0.25rem; color: $blue-500; transition-property: color; transition-duration: $transition-time-action; + width: fit-content; &.inline { display: inline-flex; diff --git a/apps/client/src/features/modals/ModalLink.tsx b/apps/client/src/common/components/external-link/ExternalLink.tsx similarity index 69% rename from apps/client/src/features/modals/ModalLink.tsx rename to apps/client/src/common/components/external-link/ExternalLink.tsx index 3e6a6b6a6..9e7f42a59 100644 --- a/apps/client/src/features/modals/ModalLink.tsx +++ b/apps/client/src/common/components/external-link/ExternalLink.tsx @@ -1,18 +1,18 @@ import { MouseEvent, ReactNode } from 'react'; import { IoOpenOutline } from '@react-icons/all-files/io5/IoOpenOutline'; -import { openLink } from '../../common/utils/linkUtils'; -import { cx } from '../../common/utils/styleUtils'; +import { openLink } from '../../utils/linkUtils'; +import { cx } from '../../utils/styleUtils'; -import style from './ModalLink.module.scss'; +import style from './ExternalLink.module.scss'; -interface ModalLinkProps { +interface ExternalLinkProps { href: string; children: ReactNode; inline?: boolean; } -export default function ModalLink(props: ModalLinkProps) { +export default function ExternalLink(props: ExternalLinkProps) { const { href, inline, children } = props; const classes = cx([style.link, inline ? style.inline : null]); diff --git a/apps/client/src/common/components/input/auto-text-area/AutoTextArea.tsx b/apps/client/src/common/components/input/auto-text-area/AutoTextArea.tsx index 5150b3e0d..1e47b8a84 100644 --- a/apps/client/src/common/components/input/auto-text-area/AutoTextArea.tsx +++ b/apps/client/src/common/components/input/auto-text-area/AutoTextArea.tsx @@ -4,6 +4,8 @@ import { Textarea, TextareaProps } from '@chakra-ui/react'; import autosize from 'autosize/dist/autosize'; export const AutoTextArea = (props: TextareaProps) => { + const { value } = props; + const ref = useRef(null); useEffect(() => { @@ -12,7 +14,7 @@ export const AutoTextArea = (props: TextareaProps) => { return () => { autosize.destroy(node); }; - }, []); + }, [value]); return (