mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-10 18:03:47 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ba94288a1 | |||
| 6c78d25af1 | |||
| 0d8c186c77 | |||
| 7ccd41eeee | |||
| 43b390a079 | |||
| 80ff3251ea | |||
| 3591b93c2d | |||
| b637621871 | |||
| ed2efe183b | |||
| 27f99b4c87 | |||
| ed4d0f060b | |||
| 7c19c52332 | |||
| b1bff22b84 | |||
| 83b9fde45e | |||
| 37974df5c6 | |||
| 161ec383fd | |||
| de38f9d467 | |||
| 8ad1f2cb38 | |||
| 143917f11a | |||
| a8756c8afe | |||
| 5501f57b45 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getontime/cli",
|
"name": "@getontime/cli",
|
||||||
"version": "3.6.2",
|
"version": "3.7.0",
|
||||||
"author": "Carlos Valente",
|
"author": "Carlos Valente",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"repository": "https://github.com/cpvalente/ontime",
|
"repository": "https://github.com/cpvalente/ontime",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-ui",
|
"name": "ontime-ui",
|
||||||
"version": "3.6.2",
|
"version": "3.7.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"@emotion/react": "^11.10.6",
|
"@emotion/react": "^11.10.6",
|
||||||
"@emotion/styled": "^11.10.6",
|
"@emotion/styled": "^11.10.6",
|
||||||
"@fontsource/open-sans": "^5.0.28",
|
"@fontsource/open-sans": "^5.0.28",
|
||||||
"@mantine/hooks": "^7.6.2",
|
"@mantine/hooks": "^7.13.3",
|
||||||
"@react-icons/all-files": "^4.1.0",
|
"@react-icons/all-files": "^4.1.0",
|
||||||
"@sentry/react": "^8.19.0",
|
"@sentry/react": "^8.19.0",
|
||||||
"@tanstack/react-query": "^5.17.9",
|
"@tanstack/react-query": "^5.17.9",
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-fast-compare": "^3.2.2",
|
"react-fast-compare": "^3.2.2",
|
||||||
"react-hook-form": "^7.49.2",
|
"react-hook-form": "^7.53.1",
|
||||||
"react-qr-code": "^2.0.12",
|
"react-qr-code": "^2.0.12",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
"web-vitals": "^3.1.1",
|
"web-vitals": "^3.1.1",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// keys in tanstack store
|
// keys in tanstack store
|
||||||
export const APP_INFO = ['appinfo'];
|
export const APP_INFO = ['appinfo'];
|
||||||
export const APP_SETTINGS = ['appSettings'];
|
export const APP_SETTINGS = ['appSettings'];
|
||||||
|
export const APP_VERSION = ['appVersion'];
|
||||||
export const CUSTOM_FIELDS = ['customFields'];
|
export const CUSTOM_FIELDS = ['customFields'];
|
||||||
export const HTTP_SETTINGS = ['httpSettings'];
|
export const HTTP_SETTINGS = ['httpSettings'];
|
||||||
export const OSC_SETTINGS = ['oscSettings'];
|
export const OSC_SETTINGS = ['oscSettings'];
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { DatabaseModel, MessageResponse, ProjectData, ProjectFileListResponse } from 'ontime-types';
|
import { DatabaseModel, MessageResponse, ProjectData, ProjectFileListResponse, QuickStartData } from 'ontime-types';
|
||||||
|
|
||||||
import { makeCSV, makeTable } from '../../features/cuesheet/cuesheetUtils';
|
import { makeCSV, makeTable } from '../../features/cuesheet/cuesheetUtils';
|
||||||
|
|
||||||
@@ -86,6 +86,14 @@ export async function createProject(
|
|||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to create a project file
|
||||||
|
*/
|
||||||
|
export async function quickProject(data: QuickStartData): Promise<MessageResponse> {
|
||||||
|
const res = await axios.post(`${dbPath}/quick`, data);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request to get the list of available project files
|
* HTTP request to get the list of available project files
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ export function maybeAxiosError(error: unknown) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return `${statusText}: ${data}`;
|
return `${statusText}: ${data}`;
|
||||||
|
} else if (error instanceof Error) {
|
||||||
|
return error.message;
|
||||||
} else {
|
} else {
|
||||||
if (typeof error !== 'string') {
|
if (typeof error !== 'string') {
|
||||||
return JSON.stringify(error);
|
return JSON.stringify(error);
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
.buttonSection {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: $element-spacing;
|
|
||||||
margin-top: -$element-spacing;
|
|
||||||
}
|
|
||||||
@@ -15,8 +15,6 @@ import {
|
|||||||
|
|
||||||
import { setClientRemote } from '../../hooks/useSocket';
|
import { setClientRemote } from '../../hooks/useSocket';
|
||||||
|
|
||||||
import style from './ClientModal.module.scss';
|
|
||||||
|
|
||||||
interface RedirectClientModalProps {
|
interface RedirectClientModalProps {
|
||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -54,14 +52,12 @@ export function RedirectClientModal(props: RedirectClientModalProps) {
|
|||||||
</InputGroup>
|
</InputGroup>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<div className={style.buttonSection}>
|
<Button size='md' variant='ontime-subtle' onClick={onClose}>
|
||||||
<Button size='md' variant='ontime-subtle' onClick={onClose}>
|
Cancel
|
||||||
Cancel
|
</Button>
|
||||||
</Button>
|
<Button size='md' variant='ontime-filled' onClick={handleRedirect} isDisabled={!canSubmit}>
|
||||||
<Button size='md' variant='ontime-filled' onClick={handleRedirect} isDisabled={!canSubmit}>
|
Submit
|
||||||
Submit
|
</Button>
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import {
|
|||||||
|
|
||||||
import { setClientRemote } from '../../hooks/useSocket';
|
import { setClientRemote } from '../../hooks/useSocket';
|
||||||
|
|
||||||
import style from './ClientModal.module.scss';
|
|
||||||
|
|
||||||
interface RenameClientModalProps {
|
interface RenameClientModalProps {
|
||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -53,14 +51,12 @@ export function RenameClientModal(props: RenameClientModalProps) {
|
|||||||
/>
|
/>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<div className={style.buttonSection}>
|
<Button size='md' variant='ontime-subtle' onClick={onClose}>
|
||||||
<Button size='md' variant='ontime-subtle' onClick={onClose}>
|
Cancel
|
||||||
Cancel
|
</Button>
|
||||||
</Button>
|
<Button size='md' variant='ontime-filled' onClick={handleRename} isDisabled={!canSubmit}>
|
||||||
<Button size='md' variant='ontime-filled' onClick={handleRename} isDisabled={!canSubmit}>
|
Submit
|
||||||
Submit
|
</Button>
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
|
|||||||
import { navigatorConstants } from '../../../viewerConfig';
|
import { navigatorConstants } from '../../../viewerConfig';
|
||||||
import { isLocalhost, serverPort } from '../../api/constants';
|
import { isLocalhost, serverPort } from '../../api/constants';
|
||||||
import useClickOutside from '../../hooks/useClickOutside';
|
import useClickOutside from '../../hooks/useClickOutside';
|
||||||
import useElectronEvent from '../../hooks/useElectronEvent';
|
import { useElectronEvent } from '../../hooks/useElectronEvent';
|
||||||
import useInfo from '../../hooks-query/useInfo';
|
import useInfo from '../../hooks-query/useInfo';
|
||||||
import { useClientStore } from '../../stores/clientStore';
|
import { useClientStore } from '../../stores/clientStore';
|
||||||
import { useViewOptionsStore } from '../../stores/viewOptions';
|
import { useViewOptionsStore } from '../../stores/viewOptions';
|
||||||
@@ -57,7 +57,7 @@ function NavigationMenu(props: NavigationMenuProps) {
|
|||||||
<RenameClientModal id={id} name={name} isOpen={isOpenRename} onClose={onCloseRename} />
|
<RenameClientModal id={id} name={name} isOpen={isOpenRename} onClose={onCloseRename} />
|
||||||
<Drawer placement='left' onClose={onClose} isOpen={isOpen} variant='ontime' data-testid='navigation__menu'>
|
<Drawer placement='left' onClose={onClose} isOpen={isOpen} variant='ontime' data-testid='navigation__menu'>
|
||||||
<DrawerOverlay />
|
<DrawerOverlay />
|
||||||
<DrawerContent maxWidth='21rem'>
|
<DrawerContent maxWidth='22rem'>
|
||||||
<DrawerHeader>
|
<DrawerHeader>
|
||||||
<DrawerCloseButton size='lg' />
|
<DrawerCloseButton size='lg' />
|
||||||
Ontime
|
Ontime
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { PropsWithChildren, useCallback, useContext } from 'react';
|
import { PropsWithChildren, useCallback, useContext } from 'react';
|
||||||
|
|
||||||
|
import { isLocalhost } from '../../api/constants';
|
||||||
import { AppContext } from '../../context/AppContext';
|
import { AppContext } from '../../context/AppContext';
|
||||||
|
|
||||||
import PinPage from './PinPage';
|
import PinPage from './PinPage';
|
||||||
@@ -9,7 +10,6 @@ interface ProtectRouteProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ProtectRoute({ permission, children }: PropsWithChildren<ProtectRouteProps>) {
|
export default function ProtectRoute({ permission, children }: PropsWithChildren<ProtectRouteProps>) {
|
||||||
const isLocal = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
|
|
||||||
const { editorAuth, operatorAuth, validate } = useContext(AppContext);
|
const { editorAuth, operatorAuth, validate } = useContext(AppContext);
|
||||||
|
|
||||||
const handleValidation = useCallback(
|
const handleValidation = useCallback(
|
||||||
@@ -29,7 +29,7 @@ export default function ProtectRoute({ permission, children }: PropsWithChildren
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLocal || hasRelevantAuth()) {
|
if (isLocalhost || hasRelevantAuth()) {
|
||||||
// eslint-disable-next-line react/jsx-no-useless-fragment -- trying to make typescript happy
|
// eslint-disable-next-line react/jsx-no-useless-fragment -- trying to make typescript happy
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { dayInMs } from 'ontime-utils';
|
||||||
|
|
||||||
|
import { version } from '../../../../../package.json';
|
||||||
|
import { APP_VERSION, isLocalhost } from '../api/constants';
|
||||||
|
import { getLatestVersion, HasUpdate } from '../api/external';
|
||||||
|
|
||||||
|
const placeholder: HasUpdate & { hasUpdates: boolean } = { url: '', version: '', hasUpdates: false };
|
||||||
|
|
||||||
|
export default function useAppVersion() {
|
||||||
|
const {
|
||||||
|
data: fetchData,
|
||||||
|
status,
|
||||||
|
isFetching,
|
||||||
|
isError,
|
||||||
|
refetch,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: APP_VERSION,
|
||||||
|
queryFn: getLatestVersion,
|
||||||
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
retry: false,
|
||||||
|
staleTime: dayInMs,
|
||||||
|
enabled: isLocalhost,
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasUpdates = fetchData?.version && !fetchData.version.includes(version);
|
||||||
|
|
||||||
|
const data = fetchData ? { ...fetchData, hasUpdates } : placeholder;
|
||||||
|
|
||||||
|
return { data, placeholder, status, isFetching, isError, refetch };
|
||||||
|
}
|
||||||
@@ -4,15 +4,20 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
const isElectron = window.process?.type === 'renderer';
|
const isElectron = window.process?.type === 'renderer';
|
||||||
const ipcRenderer = isElectron ? window.require('electron').ipcRenderer : null;
|
const ipcRenderer = isElectron ? window.require('electron').ipcRenderer : null;
|
||||||
|
|
||||||
export default function useElectronEvent() {
|
export function useElectronEvent() {
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const sendToElectron = useCallback((channel: string, args?: string | Record<string, unknown>) => {
|
const sendToElectron = useCallback((channel: string, args?: string | Record<string, unknown>) => {
|
||||||
if (isElectron && ipcRenderer) {
|
if (isElectron && ipcRenderer) {
|
||||||
ipcRenderer.send(channel, args);
|
ipcRenderer.send(channel, args);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
return { isElectron, sendToElectron };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useElectronListener() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { isElectron } = useElectronEvent();
|
||||||
|
|
||||||
// listen to requests to change the editor location
|
// listen to requests to change the editor location
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isElectron) {
|
if (isElectron) {
|
||||||
@@ -25,7 +30,5 @@ export default function useElectronEvent() {
|
|||||||
return () => {
|
return () => {
|
||||||
ipcRenderer?.removeAllListeners();
|
ipcRenderer?.removeAllListeners();
|
||||||
};
|
};
|
||||||
}, [navigate]);
|
}, [isElectron, navigate]);
|
||||||
|
|
||||||
return { isElectron, sendToElectron };
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { isOntimeEvent, MaybeString, OntimeEvent, OntimeRundownEntry, RundownCached } from 'ontime-types';
|
import { isOntimeEvent, OntimeEvent, OntimeRundownEntry, RundownCached } from 'ontime-types';
|
||||||
import {
|
import { dayInMs, MILLIS_PER_SECOND, parseUserTime, reorderArray, swapEventData } from 'ontime-utils';
|
||||||
dayInMs,
|
|
||||||
getLinkedTimes,
|
|
||||||
getPreviousEventNormal,
|
|
||||||
MILLIS_PER_SECOND,
|
|
||||||
parseUserTime,
|
|
||||||
reorderArray,
|
|
||||||
swapEventData,
|
|
||||||
} from 'ontime-utils';
|
|
||||||
|
|
||||||
import { RUNDOWN } from '../api/constants';
|
import { RUNDOWN } from '../api/constants';
|
||||||
import {
|
import {
|
||||||
@@ -270,44 +262,6 @@ export const useEventAction = () => {
|
|||||||
[_updateEventMutation, queryClient],
|
[_updateEventMutation, queryClient],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggles link of an event to the previous
|
|
||||||
*/
|
|
||||||
const linkTimer = useCallback(
|
|
||||||
async (eventId: string, linkStart: MaybeString) => {
|
|
||||||
let newEvent: Partial<OntimeEvent> = { id: eventId };
|
|
||||||
|
|
||||||
if (!linkStart) {
|
|
||||||
newEvent.linkStart = null;
|
|
||||||
} else {
|
|
||||||
const cachedRundown = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
|
||||||
if (!cachedRundown) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const currentEvent = cachedRundown.rundown[eventId] as OntimeEvent;
|
|
||||||
if (!isOntimeEvent(currentEvent)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { previousEvent } = getPreviousEventNormal(cachedRundown.rundown, cachedRundown.order, eventId);
|
|
||||||
|
|
||||||
if (!previousEvent) {
|
|
||||||
newEvent.linkStart = null;
|
|
||||||
} else {
|
|
||||||
newEvent.linkStart = previousEvent.id;
|
|
||||||
const timePatch = getLinkedTimes(currentEvent, previousEvent);
|
|
||||||
newEvent = { ...newEvent, ...timePatch };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await _updateEventMutation.mutateAsync(newEvent);
|
|
||||||
} catch (error) {
|
|
||||||
logAxiosError('Error updating event', error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[_updateEventMutation, queryClient],
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls mutation to edit multiple events
|
* Calls mutation to edit multiple events
|
||||||
* @private
|
* @private
|
||||||
@@ -614,7 +568,6 @@ export const useEventAction = () => {
|
|||||||
batchUpdateEvents,
|
batchUpdateEvents,
|
||||||
deleteEvent,
|
deleteEvent,
|
||||||
deleteAllEvents,
|
deleteAllEvents,
|
||||||
linkTimer,
|
|
||||||
reorderEvent,
|
reorderEvent,
|
||||||
swapEvents,
|
swapEvents,
|
||||||
updateEvent,
|
updateEvent,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import useAppSettingsNavigation from './useAppSettingsNavigation';
|
|||||||
import style from './AppSettings.module.scss';
|
import style from './AppSettings.module.scss';
|
||||||
|
|
||||||
export default function AppSettings() {
|
export default function AppSettings() {
|
||||||
const { close, panel, location } = useAppSettingsNavigation();
|
const { close, panel, location, setLocation } = useAppSettingsNavigation();
|
||||||
useKeyDown(close, 'Escape');
|
useKeyDown(close, 'Escape');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -26,7 +26,7 @@ export default function AppSettings() {
|
|||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<PanelList selectedPanel={panel} location={location} />
|
<PanelList selectedPanel={panel} location={location} />
|
||||||
<PanelContent onClose={close}>
|
<PanelContent onClose={close}>
|
||||||
{panel === 'project' && <ProjectPanel location={location} />}
|
{panel === 'project' && <ProjectPanel location={location} setLocation={setLocation} />}
|
||||||
{panel === 'general' && <GeneralPanel location={location} />}
|
{panel === 'general' && <GeneralPanel location={location} />}
|
||||||
{panel === 'feature_settings' && <FeatureSettingsPanel location={location} />}
|
{panel === 'feature_settings' && <FeatureSettingsPanel location={location} />}
|
||||||
{panel === 'sources' && <SourcesPanel />}
|
{panel === 'sources' && <SourcesPanel />}
|
||||||
|
|||||||
@@ -40,12 +40,16 @@ ul {
|
|||||||
background-color: $gray-1100;
|
background-color: $gray-1100;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.unsaved::before {
|
&.highlight {
|
||||||
content: '';
|
color: $red-400;
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
&::before {
|
||||||
border-radius: 3px;
|
content: '';
|
||||||
background-color: $blue-400;
|
width: 0.5em;
|
||||||
|
height: 0.5em;
|
||||||
|
border-radius: 99px;
|
||||||
|
background-color: $red-400;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.split {
|
&.split {
|
||||||
|
|||||||
@@ -1,66 +1,90 @@
|
|||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
|
import { Tooltip } from '@chakra-ui/react';
|
||||||
|
|
||||||
import { isKeyEnter } from '../../../common/utils/keyEvent';
|
import { isKeyEnter } from '../../../common/utils/keyEvent';
|
||||||
import { cx } from '../../../common/utils/styleUtils';
|
import { cx } from '../../../common/utils/styleUtils';
|
||||||
import { PanelBaseProps, settingPanels, useSettingsStore } from '../settingsStore';
|
import { tooltipDelayFast } from '../../../ontimeConfig';
|
||||||
|
import { SettingsOption, SettingsOptionId, useAppSettingsMenu } from '../useAppSettingsMenu';
|
||||||
import useAppSettingsNavigation from '../useAppSettingsNavigation';
|
import useAppSettingsNavigation from '../useAppSettingsNavigation';
|
||||||
|
|
||||||
import style from './PanelList.module.scss';
|
import style from './PanelList.module.scss';
|
||||||
|
|
||||||
|
export interface PanelBaseProps {
|
||||||
|
location?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface PanelListProps extends PanelBaseProps {
|
interface PanelListProps extends PanelBaseProps {
|
||||||
selectedPanel: string;
|
selectedPanel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PanelList({ selectedPanel, location }: PanelListProps) {
|
export default function PanelList({ selectedPanel, location }: PanelListProps) {
|
||||||
const { setLocation } = useAppSettingsNavigation();
|
const { options } = useAppSettingsMenu();
|
||||||
const { hasUnsavedChanges } = useSettingsStore();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className={style.tabs}>
|
<ul className={style.tabs}>
|
||||||
{settingPanels.map((panel) => {
|
{options.map((panel) => {
|
||||||
const unsaved = hasUnsavedChanges(panel.id);
|
const isSelected = selectedPanel === panel.id;
|
||||||
|
if (panel.highlight) {
|
||||||
const classes = cx([
|
return (
|
||||||
style.primary,
|
<Tooltip key={panel.id} label={panel.highlight} openDelay={tooltipDelayFast} shouldWrapChildren>
|
||||||
selectedPanel === panel.id ? style.active : null,
|
<PanelListItem panel={panel} location={location} isSelected={isSelected} />
|
||||||
panel.split ? style.split : null,
|
</Tooltip>
|
||||||
unsaved ? style.unsaved : null,
|
);
|
||||||
]);
|
}
|
||||||
|
return <PanelListItem key={panel.id} panel={panel} location={location} isSelected={isSelected} />;
|
||||||
return (
|
|
||||||
<Fragment key={panel.id}>
|
|
||||||
<li
|
|
||||||
key={panel.id}
|
|
||||||
onClick={() => setLocation(panel.id)}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
isKeyEnter(event) && setLocation(panel.id);
|
|
||||||
}}
|
|
||||||
className={classes}
|
|
||||||
tabIndex={0}
|
|
||||||
role='button'
|
|
||||||
>
|
|
||||||
{panel.label}
|
|
||||||
</li>
|
|
||||||
{panel.secondary?.map((secondary) => {
|
|
||||||
const id = secondary.id.split('__')[1];
|
|
||||||
const secondaryClasses = cx([style.secondary, location === id ? style.active : null]);
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={secondary.id}
|
|
||||||
onClick={() => setLocation(secondary.id)}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
isKeyEnter(event) && setLocation(secondary.id);
|
|
||||||
}}
|
|
||||||
className={secondaryClasses}
|
|
||||||
role='button'
|
|
||||||
>
|
|
||||||
{secondary.label}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PanelListItemProps {
|
||||||
|
panel: SettingsOption;
|
||||||
|
isSelected: boolean;
|
||||||
|
location?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PanelListItem(props: PanelListItemProps) {
|
||||||
|
const { panel, isSelected, location } = props;
|
||||||
|
const { setLocation } = useAppSettingsNavigation();
|
||||||
|
|
||||||
|
const classes = cx([
|
||||||
|
style.primary,
|
||||||
|
isSelected && style.active,
|
||||||
|
panel.split && style.split,
|
||||||
|
panel.highlight && style.highlight,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment key={panel.id}>
|
||||||
|
<li
|
||||||
|
key={panel.id}
|
||||||
|
onClick={() => setLocation(panel.id as SettingsOptionId)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
isKeyEnter(event) && setLocation(panel.id as SettingsOptionId);
|
||||||
|
}}
|
||||||
|
className={classes}
|
||||||
|
tabIndex={0}
|
||||||
|
role='button'
|
||||||
|
>
|
||||||
|
{panel.label}
|
||||||
|
</li>
|
||||||
|
{panel.secondary?.map((secondary) => {
|
||||||
|
const id = secondary.id.split('__')[1];
|
||||||
|
const secondaryClasses = cx([style.secondary, location === id ? style.active : null]);
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={secondary.id}
|
||||||
|
onClick={() => setLocation(secondary.id as SettingsOptionId)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
isKeyEnter(event) && setLocation(secondary.id as SettingsOptionId);
|
||||||
|
}}
|
||||||
|
className={secondaryClasses}
|
||||||
|
role='button'
|
||||||
|
>
|
||||||
|
{secondary.label}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ import { HTMLAttributes, ReactNode } from 'react';
|
|||||||
|
|
||||||
import { cx } from '../../../common/utils/styleUtils';
|
import { cx } from '../../../common/utils/styleUtils';
|
||||||
|
|
||||||
import style from './Panel.module.scss';
|
import style from './PanelUtils.module.scss';
|
||||||
|
|
||||||
export function Header({ children }: { children: ReactNode }) {
|
export function Header({ children }: { children: ReactNode }) {
|
||||||
return <h2 className={style.header}>{children}</h2>;
|
return <h2 className={style.header}>{children}</h2>;
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import { version } from '../../../../../package.json';
|
|
||||||
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
||||||
import { documentationUrl, githubUrl, websiteUrl } from '../../../../externals';
|
import { documentationUrl, githubUrl, websiteUrl } from '../../../../externals';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import CheckUpdatesButton from './CheckUpdatesButton';
|
import AppVersion from './AppVersion';
|
||||||
|
|
||||||
export default function AboutPanel() {
|
export default function AboutPanel() {
|
||||||
return (
|
return (
|
||||||
@@ -23,8 +22,7 @@ export default function AboutPanel() {
|
|||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
<Panel.Section>
|
<Panel.Section>
|
||||||
<Panel.SubHeader>Current version</Panel.SubHeader>
|
<Panel.SubHeader>Current version</Panel.SubHeader>
|
||||||
<Panel.Paragraph>{`You are currently using Ontime ${version}`}</Panel.Paragraph>
|
<AppVersion />
|
||||||
<CheckUpdatesButton version={version} />
|
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { version } from '../../../../../package.json';
|
||||||
|
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
||||||
|
import useAppVersion from '../../../../common/hooks-query/useAppVersion';
|
||||||
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
|
export default function AppVersion() {
|
||||||
|
const { data, isError } = useAppVersion();
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return (
|
||||||
|
<Panel.Paragraph>
|
||||||
|
{`You are currently using Ontime version ${version}`}
|
||||||
|
<Panel.Error>{`Could not fetch version information: ${isError}`}</Panel.Error>
|
||||||
|
</Panel.Paragraph>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.hasUpdates) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Panel.Paragraph>{`You are currently using Ontime version ${version}.`}</Panel.Paragraph>
|
||||||
|
<Panel.Paragraph>
|
||||||
|
{`A new version ${data.version} is available.`}
|
||||||
|
<ExternalLink href={data.url}> Please visit the release page to download</ExternalLink>
|
||||||
|
</Panel.Paragraph>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Panel.Paragraph>{`You are currently using the latest version of Ontime: ${version}`}</Panel.Paragraph>;
|
||||||
|
}
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { Button } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
import { getLatestVersion, HasUpdate } from '../../../../common/api/external';
|
|
||||||
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
|
||||||
|
|
||||||
import style from '../Panel.module.scss';
|
|
||||||
|
|
||||||
type CheckFail = {
|
|
||||||
error: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CheckIsLatest = {
|
|
||||||
latest: true;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CheckRemote = CheckFail | CheckIsLatest | HasUpdate;
|
|
||||||
|
|
||||||
interface CheckUpdatesButtonProps {
|
|
||||||
version: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CheckUpdatesButton(props: CheckUpdatesButtonProps) {
|
|
||||||
const { version } = props;
|
|
||||||
|
|
||||||
const [updateMessage, setUpdateMessage] = useState<CheckRemote | null>(null);
|
|
||||||
const [isFetching, setIsFetching] = useState(false);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles version comparison and returns component with message
|
|
||||||
*/
|
|
||||||
const versionCheck = async () => {
|
|
||||||
setIsFetching(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const latest = await getLatestVersion();
|
|
||||||
|
|
||||||
if (!latest.version.includes(version)) {
|
|
||||||
// new version, pass data to component
|
|
||||||
setUpdateMessage(latest);
|
|
||||||
} else {
|
|
||||||
setUpdateMessage({ latest: true });
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setUpdateMessage({ error: 'Error reaching server' });
|
|
||||||
} finally {
|
|
||||||
setIsFetching(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const disableButton = Boolean(updateMessage && 'version' in updateMessage);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
onClick={versionCheck}
|
|
||||||
variant='ontime-filled'
|
|
||||||
isLoading={isFetching}
|
|
||||||
isDisabled={disableButton}
|
|
||||||
size='sm'
|
|
||||||
maxWidth='max-content'
|
|
||||||
>
|
|
||||||
Check for updates
|
|
||||||
</Button>
|
|
||||||
<ResolveUpdateMessage updateMessage={updateMessage} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ResolveUpdateMessage(props: { updateMessage: CheckRemote | null }) {
|
|
||||||
const { updateMessage } = props;
|
|
||||||
|
|
||||||
if (updateMessage && 'error' in updateMessage) {
|
|
||||||
return <span className={style.error}>{updateMessage.error}</span>;
|
|
||||||
}
|
|
||||||
if (updateMessage && 'url' in updateMessage) {
|
|
||||||
return <ExternalLink href={updateMessage?.url}>{`New version available: ${updateMessage.version}`}</ExternalLink>;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import ClientList from './ClientList';
|
import ClientList from './ClientList';
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { RedirectClientModal } from '../../../../common/components/client-modal/
|
|||||||
import { RenameClientModal } from '../../../../common/components/client-modal/RenameClientModal';
|
import { RenameClientModal } from '../../../../common/components/client-modal/RenameClientModal';
|
||||||
import { setClientRemote } from '../../../../common/hooks/useSocket';
|
import { setClientRemote } from '../../../../common/hooks/useSocket';
|
||||||
import { useClientStore } from '../../../../common/stores/clientStore';
|
import { useClientStore } from '../../../../common/stores/clientStore';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import style from './ClientControlPanel.module.scss';
|
import style from './ClientControlPanel.module.scss';
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||||
import { PanelBaseProps } from '../../settingsStore';
|
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import CustomFields from './custom-fields/CustomFields';
|
import CustomFields from './custom-fields/CustomFields';
|
||||||
import UrlPresetsForm from './UrlPresetsForm';
|
import UrlPresetsForm from './UrlPresetsForm';
|
||||||
|
|||||||
+2
-2
@@ -13,7 +13,7 @@ import ExternalLink from '../../../../common/components/external-link/ExternalLi
|
|||||||
import useUrlPresets from '../../../../common/hooks-query/useUrlPresets';
|
import useUrlPresets from '../../../../common/hooks-query/useUrlPresets';
|
||||||
import { handleLinks } from '../../../../common/utils/linkUtils';
|
import { handleLinks } from '../../../../common/utils/linkUtils';
|
||||||
import { validateUrlPresetPath } from '../../../../common/utils/urlPresets';
|
import { validateUrlPresetPath } from '../../../../common/utils/urlPresets';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import style from './FeatureSettings.module.scss';
|
import style from './FeatureSettings.module.scss';
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ export default function UrlPresetsForm() {
|
|||||||
setError,
|
setError,
|
||||||
formState: { isSubmitting, isDirty, isValid, errors },
|
formState: { isSubmitting, isDirty, isValid, errors },
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
mode: 'onBlur',
|
mode: 'onChange',
|
||||||
defaultValues: { data },
|
defaultValues: { data },
|
||||||
values: { data },
|
values: { data },
|
||||||
resetOptions: {
|
resetOptions: {
|
||||||
|
|||||||
+7
-3
@@ -7,7 +7,7 @@ import { customFieldLabelToKey, isAlphanumericWithSpace } from 'ontime-utils';
|
|||||||
import { maybeAxiosError } from '../../../../../common/api/utils';
|
import { maybeAxiosError } from '../../../../../common/api/utils';
|
||||||
import SwatchSelect from '../../../../../common/components/input/colour-input/SwatchSelect';
|
import SwatchSelect from '../../../../../common/components/input/colour-input/SwatchSelect';
|
||||||
import useCustomFields from '../../../../../common/hooks-query/useCustomFields';
|
import useCustomFields from '../../../../../common/hooks-query/useCustomFields';
|
||||||
import * as Panel from '../../PanelUtils';
|
import * as Panel from '../../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import style from '../FeatureSettings.module.scss';
|
import style from '../FeatureSettings.module.scss';
|
||||||
|
|
||||||
@@ -67,6 +67,8 @@ export default function CustomFieldForm(props: CustomFieldsFormProps) {
|
|||||||
|
|
||||||
const colour = getValues('colour');
|
const colour = getValues('colour');
|
||||||
const canSubmit = isDirty && isValid;
|
const canSubmit = isDirty && isValid;
|
||||||
|
// if initial values are given, we can assume we are in edit mode
|
||||||
|
const isEditMode = initialKey !== undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(setupSubmit)} className={style.fieldForm}>
|
<form onSubmit={handleSubmit(setupSubmit)} className={style.fieldForm}>
|
||||||
@@ -81,7 +83,9 @@ export default function CustomFieldForm(props: CustomFieldsFormProps) {
|
|||||||
validate: (value) => {
|
validate: (value) => {
|
||||||
if (value.trim().length === 0) return 'Required field';
|
if (value.trim().length === 0) return 'Required field';
|
||||||
if (!isAlphanumericWithSpace(value)) return 'Only alphanumeric characters and space are allowed';
|
if (!isAlphanumericWithSpace(value)) return 'Only alphanumeric characters and space are allowed';
|
||||||
if (Object.keys(data).includes(value)) return 'Custom fields must be unique';
|
if (!isEditMode) {
|
||||||
|
if (isEditMode && Object.keys(data).includes(value)) return 'Custom fields must be unique';
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
@@ -92,7 +96,7 @@ export default function CustomFieldForm(props: CustomFieldsFormProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Panel.Description>Key (auto-generated value for use in Integrations and API)</Panel.Description>
|
<Panel.Description>Key (use in Integrations and API)</Panel.Description>
|
||||||
<Input {...register('key')} disabled size='sm' variant='ontime-filled' autoComplete='off' />
|
<Input {...register('key')} disabled size='sm' variant='ontime-filled' autoComplete='off' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ import { deleteCustomField, editCustomField, postCustomField } from '../../../..
|
|||||||
import ExternalLink from '../../../../../common/components/external-link/ExternalLink';
|
import ExternalLink from '../../../../../common/components/external-link/ExternalLink';
|
||||||
import useCustomFields from '../../../../../common/hooks-query/useCustomFields';
|
import useCustomFields from '../../../../../common/hooks-query/useCustomFields';
|
||||||
import { customFieldsDocsUrl } from '../../../../../externals';
|
import { customFieldsDocsUrl } from '../../../../../externals';
|
||||||
import * as Panel from '../../PanelUtils';
|
import * as Panel from '../../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import CustomFieldEntry from './CustomFieldEntry';
|
import CustomFieldEntry from './CustomFieldEntry';
|
||||||
import CustomFieldForm from './CustomFieldForm';
|
import CustomFieldForm from './CustomFieldForm';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||||
import { PanelBaseProps } from '../../settingsStore';
|
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
||||||
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
import EditorSettingsForm from '../interface-panel/EditorSettingsForm';
|
import EditorSettingsForm from '../interface-panel/EditorSettingsForm';
|
||||||
import * as Panel from '../PanelUtils';
|
|
||||||
|
|
||||||
import GeneralPanelForm from './GeneralPanelForm';
|
import GeneralPanelForm from './GeneralPanelForm';
|
||||||
import ViewSettingsForm from './ViewSettingsForm';
|
import ViewSettingsForm from './ViewSettingsForm';
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { postSettings } from '../../../../common/api/settings';
|
|||||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import useSettings from '../../../../common/hooks-query/useSettings';
|
import useSettings from '../../../../common/hooks-query/useSettings';
|
||||||
import { isOnlyNumbers } from '../../../../common/utils/regex';
|
import { isOnlyNumbers } from '../../../../common/utils/regex';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import GeneralPinInput from './GeneralPinInput';
|
import GeneralPinInput from './GeneralPinInput';
|
||||||
|
|
||||||
@@ -154,7 +154,7 @@ export default function GeneralPanelForm() {
|
|||||||
<option value='pt'>Portuguese</option>
|
<option value='pt'>Portuguese</option>
|
||||||
<option value='es'>Spanish</option>
|
<option value='es'>Spanish</option>
|
||||||
<option value='sv'>Swedish</option>
|
<option value='sv'>Swedish</option>
|
||||||
<option value='pl'>Polish</option>
|
<option value='pl'>Polish</option>
|
||||||
</Select>
|
</Select>
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
</Panel.ListGroup>
|
</Panel.ListGroup>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import ExternalLink from '../../../../common/components/external-link/ExternalLi
|
|||||||
import { PopoverPickerRHF } from '../../../../common/components/input/popover-picker/PopoverPicker';
|
import { PopoverPickerRHF } from '../../../../common/components/input/popover-picker/PopoverPicker';
|
||||||
import useInfo from '../../../../common/hooks-query/useInfo';
|
import useInfo from '../../../../common/hooks-query/useInfo';
|
||||||
import useViewSettings from '../../../../common/hooks-query/useViewSettings';
|
import useViewSettings from '../../../../common/hooks-query/useViewSettings';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import style from './GeneralPanel.module.scss';
|
import style from './GeneralPanel.module.scss';
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ export default function ViewSettingsForm() {
|
|||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field
|
<Panel.Field
|
||||||
title='Freeze timer on end'
|
title='Freeze timer on end'
|
||||||
description='Timer in views will stop from going negative and instead stop at 00:00:00'
|
description='When a timer hits 00:00:00, it freezes instead of going negative. It invalidates the End Message.'
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@@ -139,7 +139,7 @@ export default function ViewSettingsForm() {
|
|||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field
|
<Panel.Field
|
||||||
title='End message'
|
title='End message'
|
||||||
description='Message to show on negative timers if not frozen. If not provided, timer will continue'
|
description='Message for negative timers; applies only if the timer isn`t frozen on End. If no message is provided, it continues into negative time'
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
size='sm'
|
size='sm'
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { maybeAxiosError } from '../../../../common/api/utils';
|
|||||||
import { useHttpSettings, usePostHttpSettings } from '../../../../common/hooks-query/useHttpSettings';
|
import { useHttpSettings, usePostHttpSettings } from '../../../../common/hooks-query/useHttpSettings';
|
||||||
import { isKeyEscape } from '../../../../common/utils/keyEvent';
|
import { isKeyEscape } from '../../../../common/utils/keyEvent';
|
||||||
import { startsWithHttp } from '../../../../common/utils/regex';
|
import { startsWithHttp } from '../../../../common/utils/regex';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import { cycles } from './integrationUtils';
|
import { cycles } from './integrationUtils';
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ export default function HttpIntegrations() {
|
|||||||
setError,
|
setError,
|
||||||
formState: { errors, isSubmitting, isDirty, isValid },
|
formState: { errors, isSubmitting, isDirty, isValid },
|
||||||
} = useForm<HttpSettings>({
|
} = useForm<HttpSettings>({
|
||||||
mode: 'onBlur',
|
mode: 'onChange',
|
||||||
defaultValues: data,
|
defaultValues: data,
|
||||||
values: data,
|
values: data,
|
||||||
resetOptions: {
|
resetOptions: {
|
||||||
|
|||||||
+2
-2
@@ -2,8 +2,8 @@ import { Alert, AlertDescription, AlertIcon } from '@chakra-ui/react';
|
|||||||
|
|
||||||
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
||||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||||
import { PanelBaseProps } from '../../settingsStore';
|
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import HttpIntegrations from './HttpIntegrations';
|
import HttpIntegrations from './HttpIntegrations';
|
||||||
import OscIntegrations from './OscIntegrations';
|
import OscIntegrations from './OscIntegrations';
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { maybeAxiosError } from '../../../../common/api/utils';
|
|||||||
import useOscSettings, { useOscSettingsMutation } from '../../../../common/hooks-query/useOscSettings';
|
import useOscSettings, { useOscSettingsMutation } from '../../../../common/hooks-query/useOscSettings';
|
||||||
import { isKeyEscape } from '../../../../common/utils/keyEvent';
|
import { isKeyEscape } from '../../../../common/utils/keyEvent';
|
||||||
import { isASCII, isASCIIorEmpty, isIPAddress, isOnlyNumbers, startsWithSlash } from '../../../../common/utils/regex';
|
import { isASCII, isASCIIorEmpty, isIPAddress, isOnlyNumbers, startsWithSlash } from '../../../../common/utils/regex';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import { cycles } from './integrationUtils';
|
import { cycles } from './integrationUtils';
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ export default function OscIntegrations() {
|
|||||||
setError,
|
setError,
|
||||||
formState: { errors, isSubmitting, isDirty, isValid },
|
formState: { errors, isSubmitting, isDirty, isValid },
|
||||||
} = useForm<OSCSettings>({
|
} = useForm<OSCSettings>({
|
||||||
mode: 'onBlur',
|
mode: 'onChange',
|
||||||
defaultValues: data,
|
defaultValues: data,
|
||||||
values: data,
|
values: data,
|
||||||
resetOptions: {
|
resetOptions: {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { parseUserTime } from 'ontime-utils';
|
|||||||
|
|
||||||
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
|
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
|
||||||
import { editorSettingsDefaults, useEditorSettings } from '../../../../common/stores/editorSettings';
|
import { editorSettingsDefaults, useEditorSettings } from '../../../../common/stores/editorSettings';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
export default function EditorSettingsForm() {
|
export default function EditorSettingsForm() {
|
||||||
const {
|
const {
|
||||||
@@ -90,6 +90,7 @@ export default function EditorSettingsForm() {
|
|||||||
<option value={TimerType.CountUp}>Count up</option>
|
<option value={TimerType.CountUp}>Count up</option>
|
||||||
<option value={TimerType.TimeToEnd}>Time to end</option>
|
<option value={TimerType.TimeToEnd}>Time to end</option>
|
||||||
<option value={TimerType.Clock}>Clock</option>
|
<option value={TimerType.Clock}>Clock</option>
|
||||||
|
<option value={TimerType.None}>None</option>
|
||||||
</Select>
|
</Select>
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Alert, AlertDescription, AlertIcon } from '@chakra-ui/react';
|
import { Alert, AlertDescription, AlertIcon } from '@chakra-ui/react';
|
||||||
|
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import EditorSettingsForm from './EditorSettingsForm';
|
import EditorSettingsForm from './EditorSettingsForm';
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
|||||||
|
|
||||||
import { handleLinks } from '../../../../common/utils/linkUtils';
|
import { handleLinks } from '../../../../common/utils/linkUtils';
|
||||||
import Log from '../../../log/Log';
|
import Log from '../../../log/Log';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import style from './NetworkLogExport.module.scss';
|
import style from './NetworkLogExport.module.scss';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||||
import { PanelBaseProps } from '../../settingsStore';
|
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
||||||
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
import ClientControlPanel from '../client-control-panel/ClientControlPanel';
|
import ClientControlPanel from '../client-control-panel/ClientControlPanel';
|
||||||
import * as Panel from '../PanelUtils';
|
|
||||||
|
|
||||||
import InfoNif from './NetworkInterfaces';
|
import InfoNif from './NetworkInterfaces';
|
||||||
import LogExport from './NetworkLogExport';
|
import LogExport from './NetworkLogExport';
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
|||||||
import { uploadProjectFile } from '../../../../common/api/db';
|
import { uploadProjectFile } from '../../../../common/api/db';
|
||||||
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/utils';
|
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import { validateProjectFile } from '../../../../common/utils/uploadUtils';
|
import { validateProjectFile } from '../../../../common/utils/uploadUtils';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import ProjectCreateForm from './ProjectCreateForm';
|
import ProjectCreateForm from './ProjectCreateForm';
|
||||||
import ProjectList from './ProjectList';
|
import ProjectList from './ProjectList';
|
||||||
@@ -20,10 +20,10 @@ export default function ManageProjects() {
|
|||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const isCreatingProject = searchParams.get('create') === 'true';
|
const isCreatingProject = searchParams.get('new') === 'true';
|
||||||
|
|
||||||
const handleToggleCreate = () => {
|
const handleToggleCreate = () => {
|
||||||
searchParams.set('create', isCreatingProject ? 'false' : 'true');
|
searchParams.set('new', isCreatingProject ? 'false' : 'true');
|
||||||
setSearchParams(searchParams);
|
setSearchParams(searchParams);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ export default function ManageProjects() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseForm = () => {
|
const handleCloseForm = () => {
|
||||||
searchParams.delete('create');
|
searchParams.delete('new');
|
||||||
setSearchParams(searchParams);
|
setSearchParams(searchParams);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useQueryClient } from '@tanstack/react-query';
|
|||||||
import { PROJECT_LIST } from '../../../../common/api/constants';
|
import { PROJECT_LIST } from '../../../../common/api/constants';
|
||||||
import { createProject } from '../../../../common/api/db';
|
import { createProject } from '../../../../common/api/db';
|
||||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import style from './ProjectPanel.module.scss';
|
import style from './ProjectPanel.module.scss';
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { type ProjectData } from 'ontime-types';
|
|||||||
import { postProjectData } from '../../../../common/api/project';
|
import { postProjectData } from '../../../../common/api/project';
|
||||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import useProjectData from '../../../../common/hooks-query/useProjectData';
|
import useProjectData from '../../../../common/hooks-query/useProjectData';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import style from './ProjectPanel.module.scss';
|
import style from './ProjectPanel.module.scss';
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ export default function ProjectData() {
|
|||||||
variant='ontime-filled'
|
variant='ontime-filled'
|
||||||
size='sm'
|
size='sm'
|
||||||
maxLength={50}
|
maxLength={50}
|
||||||
placeholder='Your project name'
|
placeholder='Project title is shown in production views'
|
||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
{...register('title')}
|
{...register('title')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { useProjectList } from '../../../../common/hooks-query/useProjectList';
|
import { useProjectList } from '../../../../common/hooks-query/useProjectList';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import ProjectListItem, { EditMode } from './ProjectListItem';
|
import ProjectListItem, { EditMode } from './ProjectListItem';
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
} from '../../../../common/api/db';
|
} from '../../../../common/api/db';
|
||||||
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/utils';
|
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import { cx } from '../../../../common/utils/styleUtils';
|
import { cx } from '../../../../common/utils/styleUtils';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import ProjectForm, { ProjectFormValues } from './ProjectForm';
|
import ProjectForm, { ProjectFormValues } from './ProjectForm';
|
||||||
import ProjectMergeForm from './ProjectMergeForm';
|
import ProjectMergeForm from './ProjectMergeForm';
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { PROJECT_DATA } from '../../../../common/api/constants';
|
|||||||
import { getDb, patchData } from '../../../../common/api/db';
|
import { getDb, patchData } from '../../../../common/api/db';
|
||||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import { cx } from '../../../../common/utils/styleUtils';
|
import { cx } from '../../../../common/utils/styleUtils';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import { makeProjectPatch } from './project.utils';
|
import { makeProjectPatch } from './project.utils';
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,28 @@
|
|||||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||||
import { PanelBaseProps } from '../../settingsStore';
|
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
import QuickStart from '../../quick-start/QuickStart';
|
||||||
|
import type { SettingsOptionId } from '../../useAppSettingsMenu';
|
||||||
|
|
||||||
import ManageProjects from './ManageProjects';
|
import ManageProjects from './ManageProjects';
|
||||||
import ProjectData from './ProjectData';
|
import ProjectData from './ProjectData';
|
||||||
|
|
||||||
export default function ProjectPanel({ location }: PanelBaseProps) {
|
interface ProjectPanelProps extends PanelBaseProps {
|
||||||
|
setLocation: (location: SettingsOptionId) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProjectPanel({ location, setLocation }: ProjectPanelProps) {
|
||||||
const projectRef = useScrollIntoView<HTMLDivElement>('data', location);
|
const projectRef = useScrollIntoView<HTMLDivElement>('data', location);
|
||||||
const manageRef = useScrollIntoView<HTMLDivElement>('manage', location);
|
const manageRef = useScrollIntoView<HTMLDivElement>('manage', location);
|
||||||
|
|
||||||
|
const handleQuickClose = () => {
|
||||||
|
setLocation('project');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Panel.Header>Project</Panel.Header>
|
<Panel.Header>Project</Panel.Header>
|
||||||
|
<QuickStart isOpen={location === 'create'} onClose={handleQuickClose} />
|
||||||
<div ref={projectRef}>
|
<div ref={projectRef}>
|
||||||
<ProjectData />
|
<ProjectData />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import {
|
|||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
import { isLocalhost } from '../../../../common/api/constants';
|
import { isLocalhost } from '../../../../common/api/constants';
|
||||||
import useElectronEvent from '../../../../common/hooks/useElectronEvent';
|
import { useElectronEvent } from '../../../../common/hooks/useElectronEvent';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
export default function ShutdownPanel() {
|
export default function ShutdownPanel() {
|
||||||
const { isElectron, sendToElectron } = useElectronEvent();
|
const { isElectron, sendToElectron } = useElectronEvent();
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { getWorksheetNames } from '../../../../common/api/sheets';
|
|||||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import CopyTag from '../../../../common/components/copy-tag/CopyTag';
|
import CopyTag from '../../../../common/components/copy-tag/CopyTag';
|
||||||
import { openLink } from '../../../../common/utils/linkUtils';
|
import { openLink } from '../../../../common/utils/linkUtils';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import useGoogleSheet from './useGoogleSheet';
|
import useGoogleSheet from './useGoogleSheet';
|
||||||
import { useSheetStore } from './useSheetStore';
|
import { useSheetStore } from './useSheetStore';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState } from 'react';
|
|||||||
import { Button } from '@chakra-ui/react';
|
import { Button } from '@chakra-ui/react';
|
||||||
import { CustomFields, OntimeRundown } from 'ontime-types';
|
import { CustomFields, OntimeRundown } from 'ontime-types';
|
||||||
|
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import PreviewSpreadsheet from './preview/PreviewRundown';
|
import PreviewSpreadsheet from './preview/PreviewRundown';
|
||||||
import useGoogleSheet from './useGoogleSheet';
|
import useGoogleSheet from './useGoogleSheet';
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { getWorksheetNames } from '../../../../common/api/sheets';
|
import { getWorksheetNames } from '../../../../common/api/sheets';
|
||||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import { validateExcelImport } from '../../../../common/utils/uploadUtils';
|
import { validateExcelImport } from '../../../../common/utils/uploadUtils';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import ImportMapForm from './import-map/ImportMapForm';
|
import ImportMapForm from './import-map/ImportMapForm';
|
||||||
import GSheetInfo from './GSheetInfo';
|
import GSheetInfo from './GSheetInfo';
|
||||||
@@ -70,6 +70,17 @@ export default function SourcesPanel() {
|
|||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetFlow = () => {
|
||||||
|
// we purposely omit clearing the authentication status
|
||||||
|
setImportFlow('none');
|
||||||
|
setRundown(null);
|
||||||
|
setHasFile('none');
|
||||||
|
setWorksheets(null);
|
||||||
|
setCustomFields(null);
|
||||||
|
setError('');
|
||||||
|
setSheetId(null);
|
||||||
|
};
|
||||||
|
|
||||||
const openGSheetFlow = async () => {
|
const openGSheetFlow = async () => {
|
||||||
const result = await verifyAuth();
|
const result = await verifyAuth();
|
||||||
if (result) {
|
if (result) {
|
||||||
@@ -89,7 +100,7 @@ export default function SourcesPanel() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const cancelGSheetFlow = () => {
|
const cancelGSheetFlow = () => {
|
||||||
setImportFlow('none');
|
resetFlow();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmitImportPreview = async (importMap: ImportMap) => {
|
const handleSubmitImportPreview = async (importMap: ImportMap) => {
|
||||||
@@ -110,9 +121,7 @@ export default function SourcesPanel() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const cancelImportMap = async () => {
|
const cancelImportMap = async () => {
|
||||||
setImportFlow('none');
|
resetFlow();
|
||||||
setHasFile('none');
|
|
||||||
setWorksheets(null);
|
|
||||||
if (authenticationStatus === 'authenticated') {
|
if (authenticationStatus === 'authenticated') {
|
||||||
const result = await verifyAuth();
|
const result = await verifyAuth();
|
||||||
if (result) {
|
if (result) {
|
||||||
@@ -127,6 +136,7 @@ export default function SourcesPanel() {
|
|||||||
setHasFile('none');
|
setHasFile('none');
|
||||||
setWorksheets(null);
|
setWorksheets(null);
|
||||||
setCustomFields(null);
|
setCustomFields(null);
|
||||||
|
setError('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmitExport = async (importMap: ImportMap) => {
|
const handleSubmitExport = async (importMap: ImportMap) => {
|
||||||
@@ -134,15 +144,6 @@ export default function SourcesPanel() {
|
|||||||
await exportRundown(sheetId, importMap);
|
await exportRundown(sheetId, importMap);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetFlow = () => {
|
|
||||||
setImportFlow('none');
|
|
||||||
setRundown(null);
|
|
||||||
setHasFile('none');
|
|
||||||
setWorksheets(null);
|
|
||||||
setCustomFields(null);
|
|
||||||
setError('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const isExcelFlow = importFlow === 'excel';
|
const isExcelFlow = importFlow === 'excel';
|
||||||
const isGSheetFlow = importFlow === 'gsheet';
|
const isGSheetFlow = importFlow === 'gsheet';
|
||||||
const isAuthenticated = authenticationStatus === 'authenticated';
|
const isAuthenticated = authenticationStatus === 'authenticated';
|
||||||
|
|||||||
+3
-3
@@ -5,7 +5,7 @@ import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
|||||||
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
||||||
import { ImportMap, isAlphanumericWithSpace } from 'ontime-utils';
|
import { ImportMap, isAlphanumericWithSpace } from 'ontime-utils';
|
||||||
|
|
||||||
import * as Panel from '../../PanelUtils';
|
import * as Panel from '../../../panel-utils/PanelUtils';
|
||||||
import useGoogleSheet from '../useGoogleSheet';
|
import useGoogleSheet from '../useGoogleSheet';
|
||||||
import { useSheetStore } from '../useSheetStore';
|
import { useSheetStore } from '../useSheetStore';
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ export default function ImportMapForm(props: ImportMapFormProps) {
|
|||||||
register,
|
register,
|
||||||
formState: { errors, isValid },
|
formState: { errors, isValid },
|
||||||
} = useForm<NamedImportMap>({
|
} = useForm<NamedImportMap>({
|
||||||
mode: 'onBlur',
|
mode: 'onChange',
|
||||||
defaultValues: namedImportMap,
|
defaultValues: namedImportMap,
|
||||||
values: namedImportMap,
|
values: namedImportMap,
|
||||||
});
|
});
|
||||||
@@ -121,7 +121,7 @@ export default function ImportMapForm(props: ImportMapFormProps) {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Ontime field</th>
|
<th>Ontime field</th>
|
||||||
<th>From spreadsheet name</th>
|
<th>Column name in spreadsheet</th>
|
||||||
<th className={style.singleActionCell} />
|
<th className={style.singleActionCell} />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ import { millisToString } from 'ontime-utils';
|
|||||||
|
|
||||||
import Tag from '../../../../../common/components/tag/Tag';
|
import Tag from '../../../../../common/components/tag/Tag';
|
||||||
import { getAccessibleColour } from '../../../../../common/utils/styleUtils';
|
import { getAccessibleColour } from '../../../../../common/utils/styleUtils';
|
||||||
import * as Panel from '../../PanelUtils';
|
import * as Panel from '../../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import style from './PreviewRundown.module.scss';
|
import style from './PreviewRundown.module.scss';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.scrollContainer {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
Select,
|
||||||
|
Switch,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { QuickStartData } from 'ontime-types';
|
||||||
|
import { parseUserTime } from 'ontime-utils';
|
||||||
|
|
||||||
|
import { quickProject } from '../../../common/api/db';
|
||||||
|
import { invalidateAllCaches, maybeAxiosError } from '../../../common/api/utils';
|
||||||
|
import TimeInput from '../../../common/components/input/time-input/TimeInput';
|
||||||
|
import { editorSettingsDefaults, useEditorSettings } from '../../../common/stores/editorSettings';
|
||||||
|
import * as Panel from '../panel-utils/PanelUtils';
|
||||||
|
|
||||||
|
import { quickStartDefaults } from './quickStart.utils';
|
||||||
|
|
||||||
|
import style from './QuickStart.module.scss';
|
||||||
|
|
||||||
|
interface QuickStartProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function QuickStart(props: QuickStartProps) {
|
||||||
|
const { isOpen, onClose } = props;
|
||||||
|
const { defaultWarnTime, defaultDangerTime, setDangerTime, setWarnTime } = useEditorSettings();
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
register,
|
||||||
|
formState: { errors, isSubmitting, isValid },
|
||||||
|
setError,
|
||||||
|
} = useForm<QuickStartData>({
|
||||||
|
defaultValues: quickStartDefaults,
|
||||||
|
values: quickStartDefaults,
|
||||||
|
resetOptions: {
|
||||||
|
keepDirtyValues: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (formData: QuickStartData) => {
|
||||||
|
try {
|
||||||
|
if (formData.project.title === '') {
|
||||||
|
formData.project.title = 'untitled';
|
||||||
|
}
|
||||||
|
await quickProject(formData);
|
||||||
|
await invalidateAllCaches();
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
setError('root', { message: maybeAxiosError(error) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const warnTimeInMs = parseUserTime(defaultWarnTime);
|
||||||
|
const dangerTimeInMs = parseUserTime(defaultDangerTime);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} closeOnOverlayClick={false} variant='ontime'>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalContent maxWidth='max(640px, 40vw)'>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} id='quick-start'>
|
||||||
|
<ModalHeader>Create new project...</ModalHeader>
|
||||||
|
<ModalBody className={style.scrollContainer}>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<Panel.ListGroup>
|
||||||
|
<Panel.ListItem>
|
||||||
|
<Panel.Field title='Project title' description='Shown as the title in some views' />
|
||||||
|
<Input
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
maxLength={150}
|
||||||
|
placeholder='Project title'
|
||||||
|
autoComplete='off'
|
||||||
|
width='20rem'
|
||||||
|
{...register('project.title')}
|
||||||
|
/>
|
||||||
|
</Panel.ListItem>
|
||||||
|
<Panel.ListItem>
|
||||||
|
<Panel.Field
|
||||||
|
title='Time format'
|
||||||
|
description='Default time format to show in views 12 /24 hours'
|
||||||
|
error={errors.settings?.timeFormat?.message}
|
||||||
|
/>
|
||||||
|
<Select variant='ontime' size='sm' width='auto' isDisabled={false} {...register('settings.timeFormat')}>
|
||||||
|
<option value='12'>12 hours 11:00:10 PM</option>
|
||||||
|
<option value='24'>24 hours 23:00:10</option>
|
||||||
|
</Select>
|
||||||
|
</Panel.ListItem>
|
||||||
|
<Panel.ListItem>
|
||||||
|
<Panel.Field
|
||||||
|
title='Views language'
|
||||||
|
description='Language to be displayed in views'
|
||||||
|
error={errors.settings?.language?.message}
|
||||||
|
/>
|
||||||
|
<Select variant='ontime' size='sm' width='auto' isDisabled={false} {...register('settings.language')}>
|
||||||
|
<option value='en'>English</option>
|
||||||
|
<option value='fr'>French</option>
|
||||||
|
<option value='de'>German</option>
|
||||||
|
<option value='hu'>Hungarian</option>
|
||||||
|
<option value='it'>Italian</option>
|
||||||
|
<option value='no'>Norwegian</option>
|
||||||
|
<option value='pt'>Portuguese</option>
|
||||||
|
<option value='es'>Spanish</option>
|
||||||
|
<option value='sv'>Swedish</option>
|
||||||
|
<option value='pl'>Polish</option>
|
||||||
|
</Select>
|
||||||
|
</Panel.ListItem>
|
||||||
|
</Panel.ListGroup>
|
||||||
|
|
||||||
|
<Panel.ListGroup>
|
||||||
|
<Panel.ListItem>
|
||||||
|
<Panel.Field title='Warning time' description='Default threshold for warning time in an event' />
|
||||||
|
<TimeInput<'warnTime'>
|
||||||
|
name='warnTime'
|
||||||
|
submitHandler={(_field, value) => setWarnTime(value)}
|
||||||
|
time={warnTimeInMs}
|
||||||
|
placeholder={editorSettingsDefaults.warnTime}
|
||||||
|
/>
|
||||||
|
</Panel.ListItem>
|
||||||
|
<Panel.ListItem>
|
||||||
|
<Panel.Field title='Danger time' description='Default threshold for danger time in an event' />
|
||||||
|
<TimeInput<'dangerTime'>
|
||||||
|
name='dangerTime'
|
||||||
|
submitHandler={(_field, value) => setDangerTime(value)}
|
||||||
|
time={dangerTimeInMs}
|
||||||
|
placeholder={editorSettingsDefaults.dangerTime}
|
||||||
|
/>
|
||||||
|
</Panel.ListItem>
|
||||||
|
</Panel.ListGroup>
|
||||||
|
|
||||||
|
<Panel.ListGroup>
|
||||||
|
<Panel.ListItem>
|
||||||
|
<Panel.Field
|
||||||
|
title='Freeze timer on end'
|
||||||
|
description='When a timer hits 00:00:00, it freezes instead of going negative. It invalidates the End Message.'
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name='viewSettings.freezeEnd'
|
||||||
|
render={({ field: { onChange, value, ref } }) => (
|
||||||
|
<Switch variant='ontime' size='lg' isChecked={value} onChange={onChange} ref={ref} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Panel.ListItem>
|
||||||
|
<Panel.ListItem>
|
||||||
|
<Panel.Field
|
||||||
|
title='End message'
|
||||||
|
description='Message for negative timers; applies only if the timer isn`t frozen on End. If no message is provided, it continues into negative time'
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
size='sm'
|
||||||
|
autoComplete='off'
|
||||||
|
variant='ontime-filled'
|
||||||
|
maxLength={150}
|
||||||
|
width='20rem'
|
||||||
|
placeholder='Shown when timer reaches end'
|
||||||
|
{...register('viewSettings.endMessage')}
|
||||||
|
/>
|
||||||
|
</Panel.ListItem>
|
||||||
|
</Panel.ListGroup>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
{errors?.root && <Panel.Error>{errors.root.message}</Panel.Error>}
|
||||||
|
<Button variant='ontime-ghosted' size='md' onClick={onClose} isDisabled={false}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant='ontime-filled' size='md' type='submit' isDisabled={!isValid} isLoading={isSubmitting}>
|
||||||
|
Create project
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { QuickStartData } from 'ontime-types';
|
||||||
|
|
||||||
|
export const quickStartDefaults: QuickStartData = {
|
||||||
|
project: {
|
||||||
|
title: '',
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
timeFormat: '24',
|
||||||
|
language: 'en',
|
||||||
|
},
|
||||||
|
viewSettings: {
|
||||||
|
freezeEnd: false,
|
||||||
|
endMessage: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
+20
-27
@@ -1,17 +1,21 @@
|
|||||||
import { create } from 'zustand';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import useAppVersion from '../../common/hooks-query/useAppVersion';
|
||||||
|
|
||||||
export type SettingsOption = {
|
export type SettingsOption = {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
secondary?: Readonly<SettingsOption[]>;
|
secondary?: Readonly<SettingsOption[]>;
|
||||||
split?: boolean;
|
split?: boolean;
|
||||||
|
highlight?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const settingPanels: Readonly<SettingsOption[]> = [
|
const staticOptions = [
|
||||||
{
|
{
|
||||||
id: 'project',
|
id: 'project',
|
||||||
label: 'Project',
|
label: 'Project',
|
||||||
secondary: [
|
secondary: [
|
||||||
|
{ id: 'project__create', label: 'Create...' },
|
||||||
{ id: 'project__data', label: 'Project data' },
|
{ id: 'project__data', label: 'Project data' },
|
||||||
{ id: 'project__manage', label: 'Manage projects' },
|
{ id: 'project__manage', label: 'Manage projects' },
|
||||||
],
|
],
|
||||||
@@ -77,30 +81,19 @@ export const settingPanels: Readonly<SettingsOption[]> = [
|
|||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type SettingsOptionId = (typeof settingPanels)[number]['id'];
|
export type SettingsOptionId = (typeof staticOptions)[number]['id'];
|
||||||
|
|
||||||
export interface PanelBaseProps {
|
export function useAppSettingsMenu() {
|
||||||
location?: string;
|
const { data } = useAppVersion();
|
||||||
|
|
||||||
|
const options: Readonly<SettingsOption[]> = useMemo(
|
||||||
|
() =>
|
||||||
|
staticOptions.map((option) => ({
|
||||||
|
...option,
|
||||||
|
highlight: option.id === 'about' && data.hasUpdates ? 'New version available' : undefined,
|
||||||
|
})),
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { options };
|
||||||
}
|
}
|
||||||
|
|
||||||
type SettingsStore = {
|
|
||||||
unsavedChanges: Set<SettingsOptionId>;
|
|
||||||
hasUnsavedChanges: (panelId: SettingsOptionId) => boolean;
|
|
||||||
addUnsavedChanges: (panelId: SettingsOptionId) => void;
|
|
||||||
removeUnsavedChanges: (panelId: SettingsOptionId) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
|
||||||
unsavedChanges: new Set(),
|
|
||||||
hasUnsavedChanges: (panelId: SettingsOptionId) => get().unsavedChanges.has(panelId),
|
|
||||||
addUnsavedChanges: (panelId: SettingsOptionId) =>
|
|
||||||
set((state) => {
|
|
||||||
state.unsavedChanges.add(panelId);
|
|
||||||
return { unsavedChanges: new Set(state.unsavedChanges) };
|
|
||||||
}),
|
|
||||||
removeUnsavedChanges: (panelId: SettingsOptionId) =>
|
|
||||||
set((state) => {
|
|
||||||
state.unsavedChanges.delete(panelId);
|
|
||||||
return { unsavedChanges: new Set(state.unsavedChanges) };
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { SettingsOptionId } from './settingsStore';
|
import { SettingsOptionId } from './useAppSettingsMenu';
|
||||||
|
|
||||||
const settingsKey = 'settings';
|
const settingsKey = 'settings';
|
||||||
|
|
||||||
|
|||||||
@@ -21,22 +21,6 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.corner {
|
|
||||||
@include rotate-fourty-five;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
top: 0.5rem;
|
|
||||||
right: 0.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
color: $ui-white;
|
|
||||||
transition-property: color;
|
|
||||||
transition-duration: $transition-time-action;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $ontime-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.options {
|
.options {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
|
||||||
|
|
||||||
import ErrorBoundary from '../../../common/components/error-boundary/ErrorBoundary';
|
import ErrorBoundary from '../../../common/components/error-boundary/ErrorBoundary';
|
||||||
import { handleLinks } from '../../../common/utils/linkUtils';
|
import { handleLinks } from '../../../common/utils/linkUtils';
|
||||||
import { cx } from '../../../common/utils/styleUtils';
|
import { cx } from '../../../common/utils/styleUtils';
|
||||||
|
import { Corner } from '../../editors/editor-utils/EditorUtils';
|
||||||
|
|
||||||
import MessageControl from './MessageControl';
|
import MessageControl from './MessageControl';
|
||||||
|
|
||||||
import style from '../../editors/Editor.module.scss';
|
import style from '../../editors/Editor.module.scss';
|
||||||
|
|
||||||
const MessageControlExport = () => {
|
const MessageControlExport = () => {
|
||||||
|
const isExtracted = window.location.pathname.includes('/messagecontrol');
|
||||||
const classes = cx([style.content, style.contentColumnLayout]);
|
const classes = cx([style.content, style.contentColumnLayout]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.messages} data-testid='panel-messages-control'>
|
<div className={style.messages} data-testid='panel-messages-control'>
|
||||||
<IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'messagecontrol')} />
|
{!isExtracted && <Corner onClick={(event) => handleLinks(event, 'messagecontrol')} />}
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<MessageControl />
|
<MessageControl />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Tooltip } from '@chakra-ui/react';
|
import { Tooltip } from '@chakra-ui/react';
|
||||||
import { IoArrowDown } from '@react-icons/all-files/io5/IoArrowDown';
|
import { IoArrowDown } from '@react-icons/all-files/io5/IoArrowDown';
|
||||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||||
|
import { IoBan } from '@react-icons/all-files/io5/IoBan';
|
||||||
import { IoFlag } from '@react-icons/all-files/io5/IoFlag';
|
import { IoFlag } from '@react-icons/all-files/io5/IoFlag';
|
||||||
import { IoTime } from '@react-icons/all-files/io5/IoTime';
|
import { IoTime } from '@react-icons/all-files/io5/IoTime';
|
||||||
import { TimerPhase, TimerType } from 'ontime-types';
|
import { TimerPhase, TimerType } from 'ontime-types';
|
||||||
@@ -8,8 +9,9 @@ import { TimerPhase, TimerType } from 'ontime-types';
|
|||||||
import { useMessagePreview } from '../../../common/hooks/useSocket';
|
import { useMessagePreview } from '../../../common/hooks/useSocket';
|
||||||
import useViewSettings from '../../../common/hooks-query/useViewSettings';
|
import useViewSettings from '../../../common/hooks-query/useViewSettings';
|
||||||
import { handleLinks } from '../../../common/utils/linkUtils';
|
import { handleLinks } from '../../../common/utils/linkUtils';
|
||||||
import { cx } from '../../../common/utils/styleUtils';
|
import { cx, timerPlaceholder } from '../../../common/utils/styleUtils';
|
||||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||||
|
import { Corner } from '../../editors/editor-utils/EditorUtils';
|
||||||
|
|
||||||
import style from './MessageControl.module.scss';
|
import style from './MessageControl.module.scss';
|
||||||
|
|
||||||
@@ -24,6 +26,8 @@ export default function TimerPreview() {
|
|||||||
if (showTimerMessage) return 'Message';
|
if (showTimerMessage) return 'Message';
|
||||||
if (phase === TimerPhase.Pending) return 'Standby to start';
|
if (phase === TimerPhase.Pending) return 'Standby to start';
|
||||||
if (phase === TimerPhase.Overtime && data.endMessage) return 'Custom end message';
|
if (phase === TimerPhase.Overtime && data.endMessage) return 'Custom end message';
|
||||||
|
if (timerType === TimerType.TimeToEnd) return 'Time to end';
|
||||||
|
if (timerType === TimerType.None) return timerPlaceholder;
|
||||||
return 'Timer';
|
return 'Timer';
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -48,7 +52,7 @@ export default function TimerPreview() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.preview}>
|
<div className={style.preview}>
|
||||||
<IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'timer')} />
|
<Corner onClick={(event) => handleLinks(event, 'timer')} />
|
||||||
<div className={contentClasses}>
|
<div className={contentClasses}>
|
||||||
<div
|
<div
|
||||||
className={style.mainContent}
|
className={style.mainContent}
|
||||||
@@ -72,6 +76,9 @@ export default function TimerPreview() {
|
|||||||
<Tooltip label='Time type: Time to end' openDelay={tooltipDelayMid} shouldWrapChildren>
|
<Tooltip label='Time type: Time to end' openDelay={tooltipDelayMid} shouldWrapChildren>
|
||||||
<IoFlag className={style.statusIcon} data-active={timerType === TimerType.TimeToEnd} />
|
<IoFlag className={style.statusIcon} data-active={timerType === TimerType.TimeToEnd} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip label='Time type: None' openDelay={tooltipDelayMid} shouldWrapChildren>
|
||||||
|
<IoBan className={style.statusIcon} data-active={timerType === TimerType.None} />
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
|
||||||
|
|
||||||
import ErrorBoundary from '../../../common/components/error-boundary/ErrorBoundary';
|
import ErrorBoundary from '../../../common/components/error-boundary/ErrorBoundary';
|
||||||
import { handleLinks } from '../../../common/utils/linkUtils';
|
import { handleLinks } from '../../../common/utils/linkUtils';
|
||||||
|
import { Corner } from '../../editors/editor-utils/EditorUtils';
|
||||||
|
|
||||||
import PlaybackControl from './PlaybackControl';
|
import PlaybackControl from './PlaybackControl';
|
||||||
|
|
||||||
import style from '../../editors/Editor.module.scss';
|
import style from '../../editors/Editor.module.scss';
|
||||||
|
|
||||||
const TimerControlExport = () => {
|
const TimerControlExport = () => {
|
||||||
|
const isExtracted = window.location.pathname.includes('/timercontrol');
|
||||||
return (
|
return (
|
||||||
<div className={style.playback} data-testid='panel-timer-control'>
|
<div className={style.playback} data-testid='panel-timer-control'>
|
||||||
<IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'timercontrol')} />
|
{!isExtracted && <Corner onClick={(event) => handleLinks(event, 'timercontrol')} />}
|
||||||
<div className={style.content}>
|
<div className={style.content}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<PlaybackControl />
|
<PlaybackControl />
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
max-width: 18.75rem;
|
max-width: 18.75rem;
|
||||||
min-width: 5em;
|
min-width: 5em;
|
||||||
|
|
||||||
font-family: var(--font-family-override, $viewer-font-family);
|
color: $timer-color;
|
||||||
color: var(--timer-color-override, $timer-color);
|
|
||||||
line-height: 0.9em;
|
line-height: 0.9em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.1em;
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export default function Cuesheet({ data, columns, handleUpdate, selectedId, curr
|
|||||||
|
|
||||||
let rowBgColour: string | undefined;
|
let rowBgColour: string | undefined;
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
rowBgColour = 'var(--cuesheet-running-bg-override, #D20300)'; // $red-700
|
rowBgColour = '#D20300'; // $red-700
|
||||||
} else if (row.original.colour) {
|
} else if (row.original.colour) {
|
||||||
try {
|
try {
|
||||||
// the colour is user defined and might be invalid
|
// the colour is user defined and might be invalid
|
||||||
|
|||||||
@@ -4,10 +4,6 @@ $min-playback-width: 27rem;
|
|||||||
$max-playback-width: 30rem;
|
$max-playback-width: 30rem;
|
||||||
$panel-gap: 0.5rem;
|
$panel-gap: 0.5rem;
|
||||||
|
|
||||||
.corner {
|
|
||||||
@include editor.corner;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainContainer {
|
.mainContainer {
|
||||||
background-color: $ui-black;
|
background-color: $ui-black;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -31,11 +27,6 @@ $panel-gap: 0.5rem;
|
|||||||
gap: $panel-gap;
|
gap: $panel-gap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.corner {
|
|
||||||
/* we show this if the component hasnt been extracted */
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rundown,
|
.rundown,
|
||||||
.playback,
|
.playback,
|
||||||
.messages {
|
.messages {
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import { lazy, useCallback, useEffect } from 'react';
|
import { lazy, useCallback, useEffect } from 'react';
|
||||||
import { IconButton, useDisclosure } from '@chakra-ui/react';
|
import { IconButton, useDisclosure } from '@chakra-ui/react';
|
||||||
|
import { useHotkeys } from '@mantine/hooks';
|
||||||
import { IoApps } from '@react-icons/all-files/io5/IoApps';
|
import { IoApps } from '@react-icons/all-files/io5/IoApps';
|
||||||
import { IoClose } from '@react-icons/all-files/io5/IoClose';
|
import { IoClose } from '@react-icons/all-files/io5/IoClose';
|
||||||
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
|
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
|
||||||
|
|
||||||
import ProductionNavigationMenu from '../../common/components/navigation-menu/ProductionNavigationMenu';
|
import ProductionNavigationMenu from '../../common/components/navigation-menu/ProductionNavigationMenu';
|
||||||
import useElectronEvent from '../../common/hooks/useElectronEvent';
|
import { useElectronListener } from '../../common/hooks/useElectronEvent';
|
||||||
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||||
import AppSettings from '../app-settings/AppSettings';
|
import AppSettings from '../app-settings/AppSettings';
|
||||||
import useAppSettingsNavigation from '../app-settings/useAppSettingsNavigation';
|
import useAppSettingsNavigation from '../app-settings/useAppSettingsNavigation';
|
||||||
import { EditorOverview } from '../overview/Overview';
|
import { EditorOverview } from '../overview/Overview';
|
||||||
|
|
||||||
|
import Finder from './finder/Finder';
|
||||||
|
|
||||||
import styles from './Editor.module.scss';
|
import styles from './Editor.module.scss';
|
||||||
|
|
||||||
const Rundown = lazy(() => import('../rundown/RundownExport'));
|
const Rundown = lazy(() => import('../rundown/RundownExport'));
|
||||||
@@ -19,50 +22,14 @@ const MessageControl = lazy(() => import('../control/message/MessageControlExpor
|
|||||||
|
|
||||||
export default function Editor() {
|
export default function Editor() {
|
||||||
const { isOpen: isSettingsOpen, setLocation, close } = useAppSettingsNavigation();
|
const { isOpen: isSettingsOpen, setLocation, close } = useAppSettingsNavigation();
|
||||||
const { isElectron } = useElectronEvent();
|
|
||||||
const { isOpen: isMenuOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen: isMenuOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
const { isOpen: isFinderOpen, onToggle: onFinderToggle, onClose: onFinderClose } = useDisclosure();
|
||||||
const toggleSettings = useCallback(() => {
|
|
||||||
if (isSettingsOpen) {
|
|
||||||
close();
|
|
||||||
} else {
|
|
||||||
setLocation('project');
|
|
||||||
}
|
|
||||||
}, [close, isSettingsOpen, setLocation]);
|
|
||||||
|
|
||||||
// Handle keyboard shortcuts
|
|
||||||
const handleKeyPress = useCallback(
|
|
||||||
(event: KeyboardEvent) => {
|
|
||||||
// handle held key
|
|
||||||
if (event.repeat) return;
|
|
||||||
|
|
||||||
// check if the ctrl key is pressed
|
|
||||||
if (event.ctrlKey || event.metaKey) {
|
|
||||||
// ctrl + , (settings)
|
|
||||||
if (event.key === ',') {
|
|
||||||
toggleSettings();
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[toggleSettings],
|
|
||||||
);
|
|
||||||
|
|
||||||
// register ctrl + , to open settings
|
|
||||||
useEffect(() => {
|
|
||||||
if (isElectron) {
|
|
||||||
document.addEventListener('keydown', handleKeyPress);
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
if (isElectron) {
|
|
||||||
document.removeEventListener('keydown', handleKeyPress);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [handleKeyPress, isElectron]);
|
|
||||||
|
|
||||||
useWindowTitle('Editor');
|
useWindowTitle('Editor');
|
||||||
|
|
||||||
|
// we need to register the listener to change the editor location
|
||||||
|
useElectronListener();
|
||||||
|
|
||||||
// listen to shutdown request from electron process
|
// listen to shutdown request from electron process
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (window.process?.type === 'renderer') {
|
if (window.process?.type === 'renderer') {
|
||||||
@@ -72,8 +39,23 @@ export default function Editor() {
|
|||||||
}
|
}
|
||||||
}, [setLocation]);
|
}, [setLocation]);
|
||||||
|
|
||||||
|
const toggleSettings = useCallback(() => {
|
||||||
|
if (isSettingsOpen) {
|
||||||
|
close();
|
||||||
|
} else {
|
||||||
|
setLocation('project');
|
||||||
|
}
|
||||||
|
}, [close, isSettingsOpen, setLocation]);
|
||||||
|
|
||||||
|
useHotkeys([
|
||||||
|
['mod + ,', toggleSettings],
|
||||||
|
['mod + f', onFinderToggle],
|
||||||
|
['Escape', onFinderClose],
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.mainContainer} data-testid='event-editor'>
|
<div className={styles.mainContainer} data-testid='event-editor'>
|
||||||
|
<Finder isOpen={isFinderOpen} onClose={onFinderClose} />
|
||||||
<ProductionNavigationMenu isMenuOpen={isMenuOpen} onMenuClose={onClose} />
|
<ProductionNavigationMenu isMenuOpen={isMenuOpen} onMenuClose={onClose} />
|
||||||
<EditorOverview>
|
<EditorOverview>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -6,23 +6,6 @@
|
|||||||
--editor--panel__br: 8px;
|
--editor--panel__br: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin corner() {
|
|
||||||
display: none;
|
|
||||||
transform: rotate(45deg);
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
top: 0.5rem;
|
|
||||||
right: 0.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
color: $ui-white;
|
|
||||||
transition-property: color;
|
|
||||||
transition-duration: $transition-time-action;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $ontime-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin panel() {
|
@mixin panel() {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
@@ -31,9 +14,4 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: $bg-container-l2;
|
background-color: $bg-container-l2;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
|
||||||
.corner {
|
|
||||||
/* we show this if the component hasnt been extracted */
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
.corner {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: $ui-white;
|
||||||
|
transition-property: color;
|
||||||
|
transition-duration: $transition-time-action;
|
||||||
|
border-radius: 99px;
|
||||||
|
|
||||||
|
// similar styles to ontime-button-subtle
|
||||||
|
background-color: $gray-1050;
|
||||||
|
outline: 2px solid $gray-1050;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $ontime-color;
|
||||||
|
background-color: $gray-1000;
|
||||||
|
outline: 2px solid $gray-1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||||
|
import { type IconBaseProps } from '@react-icons/all-files/lib';
|
||||||
|
|
||||||
|
import { cx } from '../../../common/utils/styleUtils';
|
||||||
|
|
||||||
|
import style from './EditorUtils.module.scss';
|
||||||
|
|
||||||
|
export function Corner({ className, ...elementProps }: IconBaseProps) {
|
||||||
|
return <IoArrowUp className={cx([style.corner, className])} {...elementProps} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
.entry,
|
||||||
|
.empty,
|
||||||
|
.error {
|
||||||
|
padding-inline: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
height: 3rem;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry[data-selected='true'] {
|
||||||
|
background-color: $blue-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
color: $label-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: $error-red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data {
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas:
|
||||||
|
'index cue'
|
||||||
|
'index title';
|
||||||
|
column-gap: 1rem;
|
||||||
|
grid-template-rows: min-content 1fr;
|
||||||
|
|
||||||
|
.index {
|
||||||
|
grid-area: index;
|
||||||
|
background-color: var(--color, $gray-1000);
|
||||||
|
border-radius: 2px;
|
||||||
|
padding-block: 0.25rem;
|
||||||
|
width: 3.5rem;
|
||||||
|
align-self: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
grid-area: title;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cue {
|
||||||
|
grid-area: cue;
|
||||||
|
font-size: calc(1rem - 2px);
|
||||||
|
color: $label-gray;
|
||||||
|
max-height: 1em;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
font-size: calc(1rem - 2px);
|
||||||
|
color: $label-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.em {
|
||||||
|
color: $ui-white;
|
||||||
|
margin-inline: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollContainer {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { KeyboardEvent, useState } from 'react';
|
||||||
|
import { Input, Modal, ModalBody, ModalContent, ModalFooter, ModalOverlay } from '@chakra-ui/react';
|
||||||
|
import { useDebouncedCallback } from '@mantine/hooks';
|
||||||
|
import { isOntimeEvent, SupportedEvent } from 'ontime-types';
|
||||||
|
|
||||||
|
import { useEventSelection } from '../../rundown/useEventSelection';
|
||||||
|
|
||||||
|
import useFinder from './useFinder';
|
||||||
|
|
||||||
|
import style from './Finder.module.scss';
|
||||||
|
|
||||||
|
interface FinderProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Finder(props: FinderProps) {
|
||||||
|
const { isOpen, onClose } = props;
|
||||||
|
const { find, results, error } = useFinder();
|
||||||
|
const [selected, setSelected] = useState(0);
|
||||||
|
|
||||||
|
const setSelectedEvents = useEventSelection((state) => state.setSelectedEvents);
|
||||||
|
const debouncedFind = useDebouncedCallback(find, 100);
|
||||||
|
|
||||||
|
const navigate = (event: KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
// all operations need results
|
||||||
|
if (results.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === 'ArrowDown') {
|
||||||
|
setSelected((prev) => (prev + 1) % results.length);
|
||||||
|
}
|
||||||
|
if (event.key === 'ArrowUp') {
|
||||||
|
setSelected((prev) => (prev - 1 + results.length) % results.length);
|
||||||
|
}
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
submit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
const selectedEvent = results[selected];
|
||||||
|
setSelectedEvents({ id: selectedEvent.id, index: selectedEvent.index, selectMode: 'click' });
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMoveEvent = (event: React.MouseEvent<HTMLUListElement>) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
const li = target.closest('li');
|
||||||
|
if (li) {
|
||||||
|
const index = Number(li.dataset.index);
|
||||||
|
if (!isNaN(index)) {
|
||||||
|
setSelected(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} variant='ontime'>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent maxWidth='max(640px, 40vw)'>
|
||||||
|
<ModalBody onKeyDown={navigate}>
|
||||||
|
<Input size='lg' onChange={debouncedFind} variant='ontime-filled' placeholder='Search...' />
|
||||||
|
<ul className={style.scrollContainer} onMouseMove={handleMouseMoveEvent}>
|
||||||
|
{error && <li className={style.error}>{error}</li>}
|
||||||
|
{results.length === 0 && <li className={style.empty}>No results</li>}
|
||||||
|
{results.length > 0 &&
|
||||||
|
results.map((event, index) => {
|
||||||
|
const isSelected = selected === index;
|
||||||
|
const displayIndex = event.type === SupportedEvent.Block ? '-' : event.index;
|
||||||
|
const colour = event.type === SupportedEvent.Event ? event.colour : '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={event.id}
|
||||||
|
className={style.entry}
|
||||||
|
data-selected={isSelected}
|
||||||
|
data-index={index}
|
||||||
|
onClick={submit}
|
||||||
|
>
|
||||||
|
<div className={style.data}>
|
||||||
|
<div className={style.index} style={{ '--color': colour }}>
|
||||||
|
{displayIndex}
|
||||||
|
</div>
|
||||||
|
{isOntimeEvent(event) && <div className={style.cue}>{event.cue}</div>}
|
||||||
|
<div className={style.title}>{event.title}</div>
|
||||||
|
</div>
|
||||||
|
{isSelected && <span>Go ⏎</span>}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter className={style.footer}>
|
||||||
|
Use the keywords <span className={style.em}>cue</span>, <span className={style.em}>index</span> or
|
||||||
|
<span className={style.em}>title</span> to filter search
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
import { ChangeEvent, useState } from 'react';
|
||||||
|
import { isOntimeBlock, isOntimeEvent, MaybeString, SupportedEvent } from 'ontime-types';
|
||||||
|
|
||||||
|
import { useFlatRundown } from '../../../common/hooks-query/useRundown';
|
||||||
|
|
||||||
|
const maxResults = 12;
|
||||||
|
|
||||||
|
type FilterableBlock = {
|
||||||
|
type: SupportedEvent.Block;
|
||||||
|
id: string;
|
||||||
|
index: number;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FilterableEvent = {
|
||||||
|
type: SupportedEvent.Event;
|
||||||
|
id: string;
|
||||||
|
index: number;
|
||||||
|
eventIndex: number;
|
||||||
|
title: string;
|
||||||
|
cue: string;
|
||||||
|
colour: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FilterableEntry = FilterableBlock | FilterableEvent;
|
||||||
|
|
||||||
|
export default function useFinder() {
|
||||||
|
const { data } = useFlatRundown();
|
||||||
|
const [results, setResults] = useState<FilterableEntry[]>([]);
|
||||||
|
const [error, setError] = useState<MaybeString>(null);
|
||||||
|
|
||||||
|
/** Returns a single item with a matching index */
|
||||||
|
const searchByIndex = (searchString: string) => {
|
||||||
|
const searchIndex = Number(searchString);
|
||||||
|
if (isNaN(searchIndex) || searchIndex < 1) {
|
||||||
|
return { results: [], error: 'Invalid index' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchIndex > data.length) {
|
||||||
|
return { results: [], error: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
// indexes exposed to the UI are 1-based
|
||||||
|
let eventIndex = 1;
|
||||||
|
const results: FilterableEvent[] = [];
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const event = data[i];
|
||||||
|
if (isOntimeEvent(event)) {
|
||||||
|
if (eventIndex === searchIndex) {
|
||||||
|
results.push({
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
id: event.id,
|
||||||
|
index: i,
|
||||||
|
eventIndex,
|
||||||
|
title: event.title,
|
||||||
|
cue: event.cue,
|
||||||
|
colour: event.colour,
|
||||||
|
} satisfies FilterableEvent);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
eventIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { results, error: null };
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Returns maxResults of OntimeEvents that match the cue field */
|
||||||
|
const searchByCue = (searchString: string) => {
|
||||||
|
// indexes exposed to the UI are 1-based
|
||||||
|
let eventIndex = 1;
|
||||||
|
// limit amount of results we show
|
||||||
|
let remaining = maxResults;
|
||||||
|
const results: FilterableEvent[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
if (remaining <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const event = data[i];
|
||||||
|
if (isOntimeEvent(event)) {
|
||||||
|
if (event.cue.toLowerCase().includes(searchString)) {
|
||||||
|
remaining--;
|
||||||
|
results.push({
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
id: event.id,
|
||||||
|
index: i,
|
||||||
|
eventIndex,
|
||||||
|
title: event.title,
|
||||||
|
cue: event.cue,
|
||||||
|
colour: event.colour,
|
||||||
|
} satisfies FilterableEvent);
|
||||||
|
}
|
||||||
|
eventIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { results, error: null };
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Returns maxResults of OntimeEvents that match the title field*/
|
||||||
|
const searchByTitle = (searchString: string) => {
|
||||||
|
// indexes exposed to the UI are 1-based
|
||||||
|
let eventIndex = 1;
|
||||||
|
// limit amount of results we show
|
||||||
|
let remaining = maxResults;
|
||||||
|
const results: FilterableEntry[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
if (remaining <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = data[i];
|
||||||
|
if (isOntimeEvent(event)) {
|
||||||
|
if (event.title.toLowerCase().includes(searchString)) {
|
||||||
|
remaining--;
|
||||||
|
results.push({
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
id: event.id,
|
||||||
|
index: i,
|
||||||
|
eventIndex,
|
||||||
|
title: event.title,
|
||||||
|
cue: event.cue,
|
||||||
|
colour: event.colour,
|
||||||
|
} satisfies FilterableEvent);
|
||||||
|
}
|
||||||
|
eventIndex++;
|
||||||
|
}
|
||||||
|
if (isOntimeBlock(event)) {
|
||||||
|
if (event.title.toLowerCase().includes(searchString)) {
|
||||||
|
remaining--;
|
||||||
|
results.push({
|
||||||
|
type: SupportedEvent.Block,
|
||||||
|
id: event.id,
|
||||||
|
index: i,
|
||||||
|
title: event.title,
|
||||||
|
} satisfies FilterableBlock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { results, error: null };
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Filters the rundown to a given evaluation */
|
||||||
|
const find = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
setError('No data');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
if (event.target.value === '') {
|
||||||
|
setResults([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchValue = event.target.value.toLowerCase();
|
||||||
|
|
||||||
|
if (searchValue.startsWith('index ')) {
|
||||||
|
const searchString = searchValue.replace('index ', '').trim();
|
||||||
|
const { results, error } = searchByIndex(searchString);
|
||||||
|
setResults(results);
|
||||||
|
setError(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchValue.startsWith('cue ')) {
|
||||||
|
const searchString = searchValue.replace('cue ', '').trim();
|
||||||
|
const { results, error } = searchByCue(searchString);
|
||||||
|
setResults(results);
|
||||||
|
setError(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchString = searchValue.replace('title ', '').trim();
|
||||||
|
const { results, error } = searchByTitle(searchString);
|
||||||
|
setResults(results);
|
||||||
|
setError(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { find, results, error };
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
&.running {
|
&.running {
|
||||||
border-top: 1px solid $gray-1300;
|
border-top: 1px solid $gray-1300;
|
||||||
background-color: var(--operator-running-bg-override, $active-red);
|
background-color: $active-red;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.past {
|
&.past {
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.noColour {
|
.noColour {
|
||||||
outline: 0.15rem solid var(--operator-highlight-override, $ui-white);
|
outline: 0.15rem solid $ui-white;
|
||||||
outline-offset: -0.15rem;
|
outline-offset: -0.15rem;
|
||||||
padding-right: 0.3rem;
|
padding-right: 0.3rem;
|
||||||
color: $ui-white;
|
color: $ui-white;
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
.value {
|
.value {
|
||||||
padding-inline: 0.25rem;
|
padding-inline: 0.25rem;
|
||||||
background-color: $gray-1250;
|
background-color: $gray-1250;
|
||||||
color: var(--operator-highlight-override, $ui-white);
|
color: $ui-white;
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,12 +22,6 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.corner {
|
|
||||||
@include editor.corner;
|
|
||||||
display: inline;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
@include editor.panel;
|
@include editor.panel;
|
||||||
height: inherit;
|
height: inherit;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
|
||||||
|
|
||||||
import { ContextMenu } from '../../common/components/context-menu/ContextMenu';
|
import { ContextMenu } from '../../common/components/context-menu/ContextMenu';
|
||||||
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
||||||
import { useAppMode } from '../../common/stores/appModeStore';
|
import { useAppMode } from '../../common/stores/appModeStore';
|
||||||
import { handleLinks } from '../../common/utils/linkUtils';
|
import { handleLinks } from '../../common/utils/linkUtils';
|
||||||
import { cx } from '../../common/utils/styleUtils';
|
import { cx } from '../../common/utils/styleUtils';
|
||||||
|
import { Corner } from '../editors/editor-utils/EditorUtils';
|
||||||
|
|
||||||
import EventEditor from './event-editor/EventEditor';
|
import EventEditor from './event-editor/EventEditor';
|
||||||
import RundownWrapper from './RundownWrapper';
|
import RundownWrapper from './RundownWrapper';
|
||||||
@@ -24,7 +24,7 @@ const RundownExport = () => {
|
|||||||
<div className={style.rundown}>
|
<div className={style.rundown}>
|
||||||
<div className={style.list}>
|
<div className={style.list}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
{!isExtracted && <IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'rundown')} />}
|
{!isExtracted && <Corner onClick={(event) => handleLinks(event, 'rundown')} />}
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<RundownWrapper />
|
<RundownWrapper />
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ import { useSortable } from '@dnd-kit/sortable';
|
|||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||||
import { IoDuplicateOutline } from '@react-icons/all-files/io5/IoDuplicateOutline';
|
import { IoDuplicateOutline } from '@react-icons/all-files/io5/IoDuplicateOutline';
|
||||||
|
import { IoLink } from '@react-icons/all-files/io5/IoLink';
|
||||||
import { IoPeople } from '@react-icons/all-files/io5/IoPeople';
|
import { IoPeople } from '@react-icons/all-files/io5/IoPeople';
|
||||||
import { IoPeopleOutline } from '@react-icons/all-files/io5/IoPeopleOutline';
|
import { IoPeopleOutline } from '@react-icons/all-files/io5/IoPeopleOutline';
|
||||||
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
|
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
|
||||||
import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
|
import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
|
||||||
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
||||||
|
import { IoUnlink } from '@react-icons/all-files/io5/IoUnlink';
|
||||||
import { EndAction, MaybeString, OntimeEvent, Playback, TimerType, TimeStrategy } from 'ontime-types';
|
import { EndAction, MaybeString, OntimeEvent, Playback, TimerType, TimeStrategy } from 'ontime-types';
|
||||||
|
|
||||||
import { useContextMenu } from '../../../common/hooks/useContextMenu';
|
import { useContextMenu } from '../../../common/hooks/useContextMenu';
|
||||||
@@ -93,6 +95,24 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
const [onContextMenu] = useContextMenu<HTMLDivElement>(
|
const [onContextMenu] = useContextMenu<HTMLDivElement>(
|
||||||
selectedEvents.size > 1
|
selectedEvents.size > 1
|
||||||
? [
|
? [
|
||||||
|
{
|
||||||
|
label: 'Link to previous',
|
||||||
|
icon: IoLink,
|
||||||
|
onClick: () =>
|
||||||
|
actionHandler('update', {
|
||||||
|
field: 'linkStart',
|
||||||
|
value: 'true',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Unlink from previous',
|
||||||
|
icon: IoUnlink,
|
||||||
|
onClick: () =>
|
||||||
|
actionHandler('update', {
|
||||||
|
field: 'linkStart',
|
||||||
|
value: null,
|
||||||
|
}),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Make public',
|
label: 'Make public',
|
||||||
icon: IoPeople,
|
icon: IoPeople,
|
||||||
@@ -101,6 +121,7 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
field: 'isPublic',
|
field: 'isPublic',
|
||||||
value: true,
|
value: true,
|
||||||
}),
|
}),
|
||||||
|
withDivider: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Make private',
|
label: 'Make private',
|
||||||
@@ -114,6 +135,15 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
{ withDivider: true, label: 'Delete', icon: IoTrash, onClick: () => actionHandler('delete') },
|
{ withDivider: true, label: 'Delete', icon: IoTrash, onClick: () => actionHandler('delete') },
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
|
{
|
||||||
|
label: 'Toggle link to previous',
|
||||||
|
icon: IoLink,
|
||||||
|
onClick: () =>
|
||||||
|
actionHandler('update', {
|
||||||
|
field: 'linkStart',
|
||||||
|
value: linkStart ? null : 'true',
|
||||||
|
}),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Toggle public',
|
label: 'Toggle public',
|
||||||
icon: IoPeopleOutline,
|
icon: IoPeopleOutline,
|
||||||
@@ -122,6 +152,7 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
field: 'isPublic',
|
field: 'isPublic',
|
||||||
value: !isPublic,
|
value: !isPublic,
|
||||||
}),
|
}),
|
||||||
|
withDivider: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Add to swap',
|
label: 'Add to swap',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { memo, useEffect, useState } from 'react';
|
|||||||
import { Tooltip } from '@chakra-ui/react';
|
import { Tooltip } from '@chakra-ui/react';
|
||||||
import { IoArrowDown } from '@react-icons/all-files/io5/IoArrowDown';
|
import { IoArrowDown } from '@react-icons/all-files/io5/IoArrowDown';
|
||||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||||
|
import { IoBan } from '@react-icons/all-files/io5/IoBan';
|
||||||
import { IoFlag } from '@react-icons/all-files/io5/IoFlag';
|
import { IoFlag } from '@react-icons/all-files/io5/IoFlag';
|
||||||
import { IoPeople } from '@react-icons/all-files/io5/IoPeople';
|
import { IoPeople } from '@react-icons/all-files/io5/IoPeople';
|
||||||
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
||||||
@@ -90,6 +91,7 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
|
|||||||
delay={delay}
|
delay={delay}
|
||||||
timeStrategy={timeStrategy}
|
timeStrategy={timeStrategy}
|
||||||
linkStart={linkStart}
|
linkStart={linkStart}
|
||||||
|
timerType={timerType}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.titleSection}>
|
<div className={style.titleSection}>
|
||||||
@@ -157,8 +159,12 @@ function TimerIcon(props: { type: TimerType; className: string }) {
|
|||||||
if (type === TimerType.Clock) {
|
if (type === TimerType.Clock) {
|
||||||
return <IoTime className={className} />;
|
return <IoTime className={className} />;
|
||||||
}
|
}
|
||||||
|
if (type === TimerType.None) {
|
||||||
|
return <IoBan className={className} />;
|
||||||
|
}
|
||||||
if (type === TimerType.TimeToEnd) {
|
if (type === TimerType.TimeToEnd) {
|
||||||
return <IoFlag className={className} />;
|
const classes = cx([style.active, className]);
|
||||||
|
return <IoFlag className={classes} />;
|
||||||
}
|
}
|
||||||
return <IoArrowDown className={className} />;
|
return <IoArrowDown className={className} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,23 @@ function EventEditorEmpty() {
|
|||||||
<div className={style.prompt}>Rundown shortcuts:</div>
|
<div className={style.prompt}>Rundown shortcuts:</div>
|
||||||
<table className={style.shortcuts}>
|
<table className={style.shortcuts}>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Find in rundown</td>
|
||||||
|
<td>
|
||||||
|
<Kbd>{deviceMod}</Kbd>
|
||||||
|
<AuxKey>+</AuxKey>
|
||||||
|
<Kbd>F</Kbd>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Open Settings</td>
|
||||||
|
<td>
|
||||||
|
<Kbd>{deviceMod}</Kbd>
|
||||||
|
<AuxKey>+</AuxKey>
|
||||||
|
<Kbd>,</Kbd>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr className={style.spacer} />
|
||||||
<tr>
|
<tr>
|
||||||
<td>Select entry</td>
|
<td>Select entry</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
|
|||||||
timeStrategy={timeStrategy}
|
timeStrategy={timeStrategy}
|
||||||
linkStart={linkStart}
|
linkStart={linkStart}
|
||||||
delay={delay}
|
delay={delay}
|
||||||
|
timerType={timerType}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.delayLabel}>{delayLabel}</div>
|
<div className={style.delayLabel}>{delayLabel}</div>
|
||||||
@@ -107,6 +108,7 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
|
|||||||
<option value={TimerType.CountUp}>Count up</option>
|
<option value={TimerType.CountUp}>Count up</option>
|
||||||
<option value={TimerType.TimeToEnd}>Time to end</option>
|
<option value={TimerType.TimeToEnd}>Time to end</option>
|
||||||
<option value={TimerType.Clock}>Clock</option>
|
<option value={TimerType.Clock}>Clock</option>
|
||||||
|
<option value={TimerType.None}>None</option>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { IoLink } from '@react-icons/all-files/io5/IoLink';
|
|||||||
import { IoLockClosed } from '@react-icons/all-files/io5/IoLockClosed';
|
import { IoLockClosed } from '@react-icons/all-files/io5/IoLockClosed';
|
||||||
import { IoLockOpenOutline } from '@react-icons/all-files/io5/IoLockOpenOutline';
|
import { IoLockOpenOutline } from '@react-icons/all-files/io5/IoLockOpenOutline';
|
||||||
import { IoUnlink } from '@react-icons/all-files/io5/IoUnlink';
|
import { IoUnlink } from '@react-icons/all-files/io5/IoUnlink';
|
||||||
import { MaybeString, OntimeEvent, TimeStrategy } from 'ontime-types';
|
import { MaybeString, TimerType, TimeStrategy } from 'ontime-types';
|
||||||
|
|
||||||
import TimeInputWithButton from '../../../common/components/input/time-input/TimeInputWithButton';
|
import TimeInputWithButton from '../../../common/components/input/time-input/TimeInputWithButton';
|
||||||
import { useEventAction } from '../../../common/hooks/useEventAction';
|
import { useEventAction } from '../../../common/hooks/useEventAction';
|
||||||
@@ -22,13 +22,14 @@ interface EventBlockTimerProps {
|
|||||||
timeStrategy: TimeStrategy;
|
timeStrategy: TimeStrategy;
|
||||||
linkStart: MaybeString;
|
linkStart: MaybeString;
|
||||||
delay: number;
|
delay: number;
|
||||||
|
timerType: TimerType;
|
||||||
}
|
}
|
||||||
|
|
||||||
type TimeActions = 'timeStart' | 'timeEnd' | 'duration';
|
type TimeActions = 'timeStart' | 'timeEnd' | 'duration';
|
||||||
|
|
||||||
const TimeInputFlow = (props: EventBlockTimerProps) => {
|
const TimeInputFlow = (props: EventBlockTimerProps) => {
|
||||||
const { eventId, timeStart, timeEnd, duration, timeStrategy, linkStart, delay } = props;
|
const { eventId, timeStart, timeEnd, duration, timeStrategy, linkStart, delay, timerType } = props;
|
||||||
const { updateEvent, updateTimer, linkTimer } = useEventAction();
|
const { updateEvent, updateTimer } = useEventAction();
|
||||||
|
|
||||||
// In sync with EventEditorTimes
|
// In sync with EventEditorTimes
|
||||||
const handleSubmit = (field: TimeActions, value: string) => {
|
const handleSubmit = (field: TimeActions, value: string) => {
|
||||||
@@ -36,17 +37,22 @@ const TimeInputFlow = (props: EventBlockTimerProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleChangeStrategy = (timeStrategy: TimeStrategy) => {
|
const handleChangeStrategy = (timeStrategy: TimeStrategy) => {
|
||||||
const newEvent: Partial<OntimeEvent> = { id: eventId, timeStrategy };
|
updateEvent({ id: eventId, timeStrategy });
|
||||||
updateEvent(newEvent);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLink = (doLink: boolean) => {
|
const handleLink = (doLink: boolean) => {
|
||||||
// the string doesnt mean much for now, not more than an intent to link
|
updateEvent({ id: eventId, linkStart: doLink ? 'true' : null });
|
||||||
// we imagine that we can leverage this to create offsets p+10
|
|
||||||
linkTimer(eventId, doLink ? 'p' : null);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const overMidnight = timeStart > timeEnd;
|
const warnings = [];
|
||||||
|
if (timeStart > timeEnd) {
|
||||||
|
warnings.push('Over midnight');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timerType === TimerType.TimeToEnd) {
|
||||||
|
warnings.push('Time to end');
|
||||||
|
}
|
||||||
|
|
||||||
const hasDelay = delay !== 0;
|
const hasDelay = delay !== 0;
|
||||||
|
|
||||||
const isLockedEnd = timeStrategy === TimeStrategy.LockEnd;
|
const isLockedEnd = timeStrategy === TimeStrategy.LockEnd;
|
||||||
@@ -113,9 +119,9 @@ const TimeInputFlow = (props: EventBlockTimerProps) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TimeInputWithButton>
|
</TimeInputWithButton>
|
||||||
|
|
||||||
{overMidnight && (
|
{warnings.length > 0 && (
|
||||||
<div className={style.timerNote}>
|
<div className={style.timerNote}>
|
||||||
<Tooltip label='Over midnight' openDelay={tooltipDelayFast} variant='ontime-ondark' shouldWrapChildren>
|
<Tooltip label={warnings.join(' - ')} openDelay={tooltipDelayFast} variant='ontime-ondark' shouldWrapChildren>
|
||||||
<IoAlertCircleOutline />
|
<IoAlertCircleOutline />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ export function getTimerByType(freezeEnd: boolean, timerObject?: TimerTypeParams
|
|||||||
return Math.abs(timerObject.elapsed ?? 0);
|
return Math.abs(timerObject.elapsed ?? 0);
|
||||||
case TimerType.Clock:
|
case TimerType.Clock:
|
||||||
return timerObject.clock;
|
return timerObject.clock;
|
||||||
|
case TimerType.None:
|
||||||
|
return null;
|
||||||
default: {
|
default: {
|
||||||
const exhaustiveCheck: never = timerObject.timerType;
|
const exhaustiveCheck: never = timerObject.timerType;
|
||||||
return exhaustiveCheck;
|
return exhaustiveCheck;
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ export default function Timer(props: TimerProps) {
|
|||||||
|
|
||||||
const defaultFormat = getDefaultFormat(settings?.timeFormat);
|
const defaultFormat = getDefaultFormat(settings?.timeFormat);
|
||||||
const timerOptions = getTimerOptions(defaultFormat, customFields);
|
const timerOptions = getTimerOptions(defaultFormat, customFields);
|
||||||
|
const disableProgress = timerIsTimeOfDay || time.timerType === TimerType.None;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={showFinished ? `${baseClasses} stage-timer--finished` : baseClasses} data-testid='timer-view'>
|
<div className={showFinished ? `${baseClasses} stage-timer--finished` : baseClasses} data-testid='timer-view'>
|
||||||
@@ -205,7 +206,7 @@ export default function Timer(props: TimerProps) {
|
|||||||
{!userOptions.hideProgress && (
|
{!userOptions.hideProgress && (
|
||||||
<MultiPartProgressBar
|
<MultiPartProgressBar
|
||||||
className={isPlaying ? 'progress-container' : 'progress-container progress-container--paused'}
|
className={isPlaying ? 'progress-container' : 'progress-container progress-container--paused'}
|
||||||
now={timerIsTimeOfDay ? null : time.current}
|
now={disableProgress ? null : time.current}
|
||||||
complete={totalTime}
|
complete={totalTime}
|
||||||
normalColor={viewSettings.normalColor}
|
normalColor={viewSettings.normalColor}
|
||||||
warning={eventNow?.timeWarning}
|
warning={eventNow?.timeWarning}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ $thumb-color-hover: $gray-900;
|
|||||||
box-sizing: inherit;
|
box-sizing: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body, html {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-family: $ontime-font-family;
|
font-family: $ontime-font-family;
|
||||||
background-color: var(--background-color-override, $ui-black);
|
background-color: var(--background-color-override, $ui-black);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const ontimeModal = {
|
|||||||
minHeight: 'min(200px, 10vh)',
|
minHeight: 'min(200px, 10vh)',
|
||||||
backgroundColor: '#202020', // $gray-1250
|
backgroundColor: '#202020', // $gray-1250
|
||||||
color: '#fefefe', // $gray-50
|
color: '#fefefe', // $gray-50
|
||||||
|
border: '1px solid #2d2d2d', // $gray-1100
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
padding: '1rem',
|
padding: '1rem',
|
||||||
@@ -24,6 +25,8 @@ export const ontimeModal = {
|
|||||||
color: '#fefefe', // $gray-50
|
color: '#fefefe', // $gray-50
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
padding: '0.5rem',
|
padding: '1rem',
|
||||||
|
display: 'flex',
|
||||||
|
gap: '0.5rem',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime",
|
"name": "ontime",
|
||||||
"version": "3.6.2",
|
"version": "3.7.0",
|
||||||
"author": "Carlos Valente",
|
"author": "Carlos Valente",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"repository": "https://github.com/cpvalente/ontime",
|
"repository": "https://github.com/cpvalente/ontime",
|
||||||
|
|||||||
@@ -81,14 +81,18 @@ function makeFileMenu(serverUrl, redirectWindow, download) {
|
|||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'New project...',
|
label: 'New project...',
|
||||||
click: () => redirectWindow('/editor?settings=project__manage&create=true'),
|
click: () => redirectWindow('/editor?settings=project__manage&new=true'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Edit project info',
|
label: 'Quick start...',
|
||||||
|
click: () => redirectWindow('/editor?settings=project__create'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Edit project info...',
|
||||||
click: () => redirectWindow('/editor?settings=project__data'),
|
click: () => redirectWindow('/editor?settings=project__data'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Manage projects...',
|
label: 'Manage projects',
|
||||||
click: () => redirectWindow('/editor?settings=project__manage'),
|
click: () => redirectWindow('/editor?settings=project__manage'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "ontime-server",
|
"name": "ontime-server",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"version": "3.6.2",
|
"version": "3.7.0",
|
||||||
"exports": "./src/index.js",
|
"exports": "./src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@googleapis/sheets": "^5.0.5",
|
"@googleapis/sheets": "^5.0.5",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DatabaseModel, ErrorResponse, MessageResponse, ProjectData, ProjectFileListResponse } from 'ontime-types';
|
import { DatabaseModel, ErrorResponse, MessageResponse, ProjectFileListResponse } from 'ontime-types';
|
||||||
import { getErrorMessage } from 'ontime-utils';
|
import { getErrorMessage } from 'ontime-utils';
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
@@ -30,17 +30,17 @@ export async function patchPartialProjectFile(req: Request, res: Response<Databa
|
|||||||
* or a 500 status with an error message in case of an exception.
|
* or a 500 status with an error message in case of an exception.
|
||||||
*/
|
*/
|
||||||
export async function createProjectFile(req: Request, res: Response<{ filename: string } | ErrorResponse>) {
|
export async function createProjectFile(req: Request, res: Response<{ filename: string } | ErrorResponse>) {
|
||||||
const newProjectData: ProjectData = {
|
|
||||||
title: req.body?.title ?? '',
|
|
||||||
description: req.body?.description ?? '',
|
|
||||||
publicUrl: req.body?.publicUrl ?? '',
|
|
||||||
publicInfo: req.body?.publicInfo ?? '',
|
|
||||||
backstageUrl: req.body?.backstageUrl ?? '',
|
|
||||||
backstageInfo: req.body?.backstageInfo ?? '',
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newFileName = await projectService.createProject(req.body.filename, newProjectData);
|
const newFileName = await projectService.createProject(req.body.filename, {
|
||||||
|
project: {
|
||||||
|
title: req.body?.title ?? '',
|
||||||
|
description: req.body?.description ?? '',
|
||||||
|
publicUrl: req.body?.publicUrl ?? '',
|
||||||
|
publicInfo: req.body?.publicInfo ?? '',
|
||||||
|
backstageUrl: req.body?.backstageUrl ?? '',
|
||||||
|
backstageInfo: req.body?.backstageInfo ?? '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
res.status(200).send({
|
res.status(200).send({
|
||||||
filename: newFileName,
|
filename: newFileName,
|
||||||
@@ -51,6 +51,21 @@ export async function createProjectFile(req: Request, res: Response<{ filename:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and loads a new project with partial DataBase data
|
||||||
|
*/
|
||||||
|
export async function quickProjectFile(req: Request, res: Response<{ filename: string } | ErrorResponse>) {
|
||||||
|
try {
|
||||||
|
const filename = await projectService.createProject(req.body.project.title, req.body);
|
||||||
|
res.status(200).send({
|
||||||
|
filename,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const message = getErrorMessage(error);
|
||||||
|
res.status(500).send({ message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows downloading of current project file
|
* Allows downloading of current project file
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
patchPartialProjectFile,
|
patchPartialProjectFile,
|
||||||
postProjectFile,
|
postProjectFile,
|
||||||
projectDownload,
|
projectDownload,
|
||||||
|
quickProjectFile,
|
||||||
renameProjectFile,
|
renameProjectFile,
|
||||||
} from './db.controller.js';
|
} from './db.controller.js';
|
||||||
import { uploadProjectFile } from './db.middleware.js';
|
import { uploadProjectFile } from './db.middleware.js';
|
||||||
@@ -19,6 +20,7 @@ import {
|
|||||||
validateFilenameBody,
|
validateFilenameBody,
|
||||||
validateFilenameParam,
|
validateFilenameParam,
|
||||||
validateNewFilenameBody,
|
validateNewFilenameBody,
|
||||||
|
validateQuickProject,
|
||||||
} from './db.validation.js';
|
} from './db.validation.js';
|
||||||
|
|
||||||
export const router = express.Router();
|
export const router = express.Router();
|
||||||
@@ -29,6 +31,7 @@ router.post('/upload', uploadProjectFile, postProjectFile);
|
|||||||
|
|
||||||
router.patch('/', validatePatchProject, patchPartialProjectFile);
|
router.patch('/', validatePatchProject, patchPartialProjectFile);
|
||||||
router.post('/new', validateFilenameBody, validateNewProject, createProjectFile);
|
router.post('/new', validateFilenameBody, validateNewProject, createProjectFile);
|
||||||
|
router.post('/quick', validateQuickProject, quickProjectFile);
|
||||||
|
|
||||||
router.get('/all', listProjects);
|
router.get('/all', listProjects);
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,28 @@ export const validateNewProject = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Validates request for a quick project.
|
||||||
|
*/
|
||||||
|
export const validateQuickProject = [
|
||||||
|
// Project fields
|
||||||
|
body('project.title').isString().trim(),
|
||||||
|
|
||||||
|
// Settings fields
|
||||||
|
body('settings.timeFormat').optional().isIn(['12', '24']),
|
||||||
|
body('settings.language').optional().isString().trim(),
|
||||||
|
|
||||||
|
// ViewSettings fields
|
||||||
|
body('viewSettings.freezeEnd').optional().isBoolean(),
|
||||||
|
body('viewSettings.endMessage').optional().isString().trim(),
|
||||||
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Validates request for pathing data in the project.
|
* @description Validates request for pathing data in the project.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,11 +4,20 @@ import { DatabaseModel } from 'ontime-types';
|
|||||||
* Merges a partial ontime project into a given ontime project
|
* Merges a partial ontime project into a given ontime project
|
||||||
*/
|
*/
|
||||||
export function safeMerge(existing: DatabaseModel, newData: Partial<DatabaseModel>): DatabaseModel {
|
export function safeMerge(existing: DatabaseModel, newData: Partial<DatabaseModel>): DatabaseModel {
|
||||||
const { rundown, project, settings, viewSettings, urlPresets, customFields, osc, http } = newData || {};
|
const {
|
||||||
|
rundown = existing.rundown,
|
||||||
|
project = {},
|
||||||
|
settings = {},
|
||||||
|
viewSettings = {},
|
||||||
|
urlPresets = existing.urlPresets,
|
||||||
|
customFields = existing.customFields,
|
||||||
|
osc = {},
|
||||||
|
http = {},
|
||||||
|
} = newData;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...existing,
|
...existing,
|
||||||
rundown: rundown ?? existing.rundown,
|
rundown,
|
||||||
project: { ...existing.project, ...project },
|
project: { ...existing.project, ...project },
|
||||||
settings: { ...existing.settings, ...settings },
|
settings: { ...existing.settings, ...settings },
|
||||||
viewSettings: { ...existing.viewSettings, ...viewSettings },
|
viewSettings: { ...existing.viewSettings, ...viewSettings },
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DatabaseModel, LogOrigin, ProjectData, ProjectFileListResponse } from 'ontime-types';
|
import { DatabaseModel, LogOrigin, ProjectFileListResponse } from 'ontime-types';
|
||||||
import { getErrorMessage } from 'ontime-utils';
|
import { getErrorMessage } from 'ontime-utils';
|
||||||
|
|
||||||
import { copyFile, rename } from 'fs/promises';
|
import { copyFile, rename } from 'fs/promises';
|
||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
moveCorruptFile,
|
moveCorruptFile,
|
||||||
parseJsonFile,
|
parseJsonFile,
|
||||||
} from './projectServiceUtils.js';
|
} from './projectServiceUtils.js';
|
||||||
|
import { safeMerge } from '../../classes/data-provider/DataProvider.utils.js';
|
||||||
|
|
||||||
// init dependencies
|
// init dependencies
|
||||||
init();
|
init();
|
||||||
@@ -263,14 +264,8 @@ export async function renameProjectFile(originalFile: string, newFilename: strin
|
|||||||
/**
|
/**
|
||||||
* Creates a new project file and applies its result
|
* Creates a new project file and applies its result
|
||||||
*/
|
*/
|
||||||
export async function createProject(filename: string, projectData: ProjectData) {
|
export async function createProject(filename: string, initialData: Partial<DatabaseModel>) {
|
||||||
const data: DatabaseModel = {
|
const data = safeMerge(dbModel, initialData);
|
||||||
...dbModel,
|
|
||||||
project: {
|
|
||||||
...dbModel.project,
|
|
||||||
...projectData,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const uniqueFileName = generateUniqueFileName(publicDir.projectsDir, filename);
|
const uniqueFileName = generateUniqueFileName(publicDir.projectsDir, filename);
|
||||||
const newFile = getPathToProject(uniqueFileName);
|
const newFile = getPathToProject(uniqueFileName);
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ export function handleLink(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sometimes the client cannot set the previous event
|
||||||
|
if (mutableEvent.linkStart === 'true') {
|
||||||
|
mutableEvent.linkStart = linkedEvent.id;
|
||||||
|
}
|
||||||
|
|
||||||
links[linkedEvent.id] = mutableEvent.id;
|
links[linkedEvent.id] = mutableEvent.id;
|
||||||
|
|
||||||
const timePatch = getLinkedTimes(mutableEvent, linkedEvent);
|
const timePatch = getLinkedTimes(mutableEvent, linkedEvent);
|
||||||
|
|||||||
@@ -43,14 +43,7 @@
|
|||||||
|
|
||||||
/** Colour used for external message and aux timer in /timer */
|
/** Colour used for external message and aux timer in /timer */
|
||||||
--external-color-override: #161616;
|
--external-color-override: #161616;
|
||||||
|
|
||||||
/** View specific features: /cuesheet */
|
|
||||||
--cuesheet-running-bg-override: #D20300;
|
|
||||||
|
|
||||||
/** View specific features: /op */
|
|
||||||
--operator-running-bg-override: #D20300;
|
|
||||||
--operator-highlight-override: #FFAB33;
|
|
||||||
|
|
||||||
/** View specific features: /studio */
|
/** View specific features: /studio */
|
||||||
--studio-active: #101010;
|
--studio-active: #101010;
|
||||||
--studio-idle: #cfcfcf;
|
--studio-idle: #cfcfcf;
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime",
|
"name": "ontime",
|
||||||
"version": "3.6.2",
|
"version": "3.7.0",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ontime",
|
"ontime",
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { type DatabaseModel } from '../../definitions/DataModel.type.js';
|
||||||
|
|
||||||
|
export interface QuickStartData {
|
||||||
|
project: Pick<DatabaseModel['project'], 'title'>;
|
||||||
|
settings: Pick<DatabaseModel['settings'], 'timeFormat' | 'language'>;
|
||||||
|
viewSettings: Pick<DatabaseModel['viewSettings'], 'freezeEnd' | 'endMessage'>;
|
||||||
|
}
|
||||||
@@ -3,4 +3,5 @@ export enum TimerType {
|
|||||||
CountUp = 'count-up',
|
CountUp = 'count-up',
|
||||||
TimeToEnd = 'time-to-end',
|
TimeToEnd = 'time-to-end',
|
||||||
Clock = 'clock',
|
Clock = 'clock',
|
||||||
|
None = 'none',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export type {
|
|||||||
RundownPaginated,
|
RundownPaginated,
|
||||||
SessionStats,
|
SessionStats,
|
||||||
} from './api/ontime-controller/BackendResponse.type.js';
|
} from './api/ontime-controller/BackendResponse.type.js';
|
||||||
|
export type { QuickStartData } from './api/db/db.type.js';
|
||||||
export type { RundownCached, NormalisedRundown } from './api/rundown-controller/BackendResponse.type.js';
|
export type { RundownCached, NormalisedRundown } from './api/rundown-controller/BackendResponse.type.js';
|
||||||
|
|
||||||
// SERVER RUNTIME
|
// SERVER RUNTIME
|
||||||
|
|||||||
Generated
+15
-12
@@ -18,6 +18,9 @@ catalogs:
|
|||||||
eslint:
|
eslint:
|
||||||
specifier: 8.56.0
|
specifier: 8.56.0
|
||||||
version: 8.56.0
|
version: 8.56.0
|
||||||
|
eslint-config-prettier:
|
||||||
|
specifier: 9.1.0
|
||||||
|
version: 9.1.0
|
||||||
eslint-plugin-prettier:
|
eslint-plugin-prettier:
|
||||||
specifier: 5.1.3
|
specifier: 5.1.3
|
||||||
version: 5.1.3
|
version: 5.1.3
|
||||||
@@ -102,8 +105,8 @@ importers:
|
|||||||
specifier: ^5.0.28
|
specifier: ^5.0.28
|
||||||
version: 5.0.28
|
version: 5.0.28
|
||||||
'@mantine/hooks':
|
'@mantine/hooks':
|
||||||
specifier: ^7.6.2
|
specifier: ^7.13.3
|
||||||
version: 7.6.2(react@18.3.1)
|
version: 7.13.3(react@18.3.1)
|
||||||
'@react-icons/all-files':
|
'@react-icons/all-files':
|
||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0(react@18.3.1)
|
version: 4.1.0(react@18.3.1)
|
||||||
@@ -147,8 +150,8 @@ importers:
|
|||||||
specifier: ^3.2.2
|
specifier: ^3.2.2
|
||||||
version: 3.2.2
|
version: 3.2.2
|
||||||
react-hook-form:
|
react-hook-form:
|
||||||
specifier: ^7.49.2
|
specifier: ^7.53.1
|
||||||
version: 7.49.2(react@18.3.1)
|
version: 7.53.1(react@18.3.1)
|
||||||
react-qr-code:
|
react-qr-code:
|
||||||
specifier: ^2.0.12
|
specifier: ^2.0.12
|
||||||
version: 2.0.12(react@18.3.1)
|
version: 2.0.12(react@18.3.1)
|
||||||
@@ -1673,8 +1676,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==}
|
resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
|
|
||||||
'@mantine/hooks@7.6.2':
|
'@mantine/hooks@7.13.3':
|
||||||
resolution: {integrity: sha512-ZrOgrZHoIGCDKrr2/9njDgK0al+jjusYQFlmR0YyEFyRtgY6eNSI4zuYLcAPx1haHmUm5RsLBrqY6Iy/TLdGXA==}
|
resolution: {integrity: sha512-r2c+Z8CdvPKFeOwg6mSJmxOp9K/ave5ZFR7eJbgv4wQU8K1CAS5f5ven9K5uUX8Vf9B5dFnSaSgYp9UY3vOWTw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.2.0
|
react: ^18.2.0
|
||||||
|
|
||||||
@@ -4508,11 +4511,11 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
react-hook-form@7.49.2:
|
react-hook-form@7.53.1:
|
||||||
resolution: {integrity: sha512-TZcnSc17+LPPVpMRIDNVITY6w20deMdNi6iehTFLV1x8SqThXGwu93HjlUVU09pzFgZH7qZOvLMM7UYf2ShAHA==}
|
resolution: {integrity: sha512-6aiQeBda4zjcuaugWvim9WsGqisoUk+etmFEsSUMm451/Ic8L/UAb7sRtMj3V+Hdzm6mMjU1VhiSzYUZeBm0Vg==}
|
||||||
engines: {node: '>=18', pnpm: '8'}
|
engines: {node: '>=18.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.8.0 || ^17 || ^18
|
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||||
|
|
||||||
react-is@16.13.1:
|
react-is@16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
@@ -6796,7 +6799,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@mantine/hooks@7.6.2(react@18.3.1)':
|
'@mantine/hooks@7.13.3(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
|
|
||||||
@@ -10094,7 +10097,7 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.0.26
|
'@types/react': 18.0.26
|
||||||
|
|
||||||
react-hook-form@7.49.2(react@18.3.1):
|
react-hook-form@7.53.1(react@18.3.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user