mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-09 16:19:47 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 67ebd12a94 | |||
| e963e183db | |||
| ac4257ece9 | |||
| 8e111512d8 |
@@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@base-ui-components/react": "1.0.0-beta.1",
|
"@base-ui-components/react": "1.0.0-beta.0",
|
||||||
"@chakra-ui/react": "^2.7.0",
|
"@chakra-ui/react": "^2.7.0",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
@@ -13,7 +13,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": "^8.1.2",
|
"@mantine/hooks": "^7.17.2",
|
||||||
"@sentry/react": "^8.43.0",
|
"@sentry/react": "^8.43.0",
|
||||||
"@table-nav/react": "^0.0.7",
|
"@table-nav/react": "^0.0.7",
|
||||||
"@tanstack/react-query": "^5.62.7",
|
"@tanstack/react-query": "^5.62.7",
|
||||||
@@ -34,7 +34,9 @@
|
|||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
"react-simple-code-editor": "^0.14.1",
|
"react-simple-code-editor": "^0.14.1",
|
||||||
"web-vitals": "^3.1.1",
|
"web-vitals": "^3.1.1",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.3",
|
||||||
|
"lexical": "^0.17.0",
|
||||||
|
"@lexical/react": "^0.17.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
|
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export const AUTOMATION = ['automation'];
|
|||||||
export const CUSTOM_FIELDS = ['customFields'];
|
export const CUSTOM_FIELDS = ['customFields'];
|
||||||
export const PROJECT_DATA = ['project'];
|
export const PROJECT_DATA = ['project'];
|
||||||
export const PROJECT_LIST = ['projectList'];
|
export const PROJECT_LIST = ['projectList'];
|
||||||
export const PROJECT_RUNDOWNS = ['projectRundowns'];
|
|
||||||
export const RUNDOWN = ['rundown'];
|
export const RUNDOWN = ['rundown'];
|
||||||
export const RUNTIME = ['runtimeStore'];
|
export const RUNTIME = ['runtimeStore'];
|
||||||
export const URL_PRESETS = ['urlpresets'];
|
export const URL_PRESETS = ['urlpresets'];
|
||||||
|
|||||||
@@ -1,45 +1,38 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { CustomField, CustomFieldKey } from 'ontime-types'; // Removed CustomFields
|
import { CustomField, CustomFieldKey, CustomFields } from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
|
|
||||||
// Define CustomFieldWithKey for client-side usage
|
|
||||||
export type CustomFieldWithKey = CustomField & { key: CustomFieldKey };
|
|
||||||
|
|
||||||
const customFieldsPath = `${apiEntryUrl}/custom-fields`;
|
const customFieldsPath = `${apiEntryUrl}/custom-fields`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests list of known custom fields, sorted by order
|
* Requests list of known custom fields
|
||||||
*/
|
*/
|
||||||
export async function getCustomFields(): Promise<CustomFieldWithKey[]> {
|
export async function getCustomFields(): Promise<CustomFields> {
|
||||||
const res = await axios.get<CustomFieldWithKey[]>(customFieldsPath);
|
const res = await axios.get(customFieldsPath);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets list of known custom fields
|
* Sets list of known custom fields
|
||||||
* Returns the updated list, sorted by order
|
|
||||||
*/
|
*/
|
||||||
export async function postCustomField(newField: CustomField): Promise<CustomFieldWithKey[]> {
|
export async function postCustomField(newField: CustomField): Promise<CustomFields> {
|
||||||
const res = await axios.post<CustomFieldWithKey[]>(customFieldsPath, { ...newField });
|
const res = await axios.post(customFieldsPath, { ...newField });
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Edits single custom field
|
* Edits single custom field
|
||||||
* Returns the updated list, sorted by order
|
|
||||||
*/
|
*/
|
||||||
export async function editCustomField(key: CustomFieldKey, newField: Partial<CustomField>): Promise<CustomFieldWithKey[]> {
|
export async function editCustomField(key: CustomFieldKey, newField: CustomField): Promise<CustomFields> {
|
||||||
// Ensure newField can include 'order' by using Partial<CustomField>
|
const res = await axios.put(`${customFieldsPath}/${key}`, { ...newField });
|
||||||
const res = await axios.put<CustomFieldWithKey[]>(`${customFieldsPath}/${key}`, { ...newField });
|
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes single custom field
|
* Deletes single custom field
|
||||||
* Returns the updated list, sorted by order
|
|
||||||
*/
|
*/
|
||||||
export async function deleteCustomField(key: CustomFieldKey): Promise<CustomFieldWithKey[]> {
|
export async function deleteCustomField(key: CustomFieldKey): Promise<CustomFields> {
|
||||||
const res = await axios.delete<CustomFieldWithKey[]>(`${customFieldsPath}/${key}`);
|
const res = await axios.delete(`${customFieldsPath}/${key}`);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
.subtle {
|
.subtle {
|
||||||
background: $gray-1050;
|
background: $gray-1050;
|
||||||
color: $blue-400;
|
color: $blue-400;
|
||||||
line-height: 1em;
|
|
||||||
|
|
||||||
&:hover:not(:disabled):not(:active) {
|
&:hover:not(:disabled):not(:active) {
|
||||||
background: $gray-1000;
|
background: $gray-1000;
|
||||||
@@ -82,25 +81,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ghosted {
|
.ghosted {
|
||||||
background: transparent;
|
|
||||||
color: $blue-500;
|
|
||||||
|
|
||||||
&:hover:not(:disabled):not(:active) {
|
|
||||||
background: $gray-1000;
|
|
||||||
color: $blue-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active:not(:disabled) {
|
|
||||||
background: $gray-1100;
|
|
||||||
border-color: $gray-1250;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: $opacity-disabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ghosted-white {
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: $ui-white;
|
color: $ui-white;
|
||||||
|
|
||||||
@@ -117,24 +97,4 @@
|
|||||||
&:disabled {
|
&:disabled {
|
||||||
opacity: $opacity-disabled;
|
opacity: $opacity-disabled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.ghosted-destructive {
|
|
||||||
background: transparent;
|
|
||||||
color: $red-500;
|
|
||||||
|
|
||||||
&:hover:not(:disabled):not(:active) {
|
|
||||||
background: $gray-1000;
|
|
||||||
color: $red-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active:not(:disabled) {
|
|
||||||
background: $gray-1100;
|
|
||||||
border-color: $gray-1250;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: $opacity-disabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
@import './BaseButtonStyles.module.scss';
|
@import './BaseButtonStyles.module.scss';
|
||||||
|
|
||||||
.baseButton {
|
.baseButton {
|
||||||
position: relative;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -26,36 +25,6 @@
|
|||||||
outline: 2px solid $blue-500;
|
outline: 2px solid $blue-500;
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.loading {
|
|
||||||
cursor: wait;
|
|
||||||
.content {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loadingOverlay {
|
|
||||||
position: absolute;
|
|
||||||
display: grid;
|
|
||||||
place-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
stroke-dasharray: 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.small {
|
.small {
|
||||||
|
|||||||
@@ -1,52 +1,29 @@
|
|||||||
import { ButtonHTMLAttributes, forwardRef } from 'react';
|
import { ButtonHTMLAttributes, forwardRef } from 'react';
|
||||||
import { IoEllipseOutline } from 'react-icons/io5';
|
|
||||||
|
|
||||||
import { cx } from '../../utils/styleUtils';
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|
||||||
import style from './Button.module.scss';
|
import style from './Button.module.scss';
|
||||||
|
|
||||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
variant?:
|
variant?: 'primary' | 'subtle' | 'subtle-white' | 'destructive' | 'subtle-destructive' | 'ghosted';
|
||||||
| 'primary'
|
|
||||||
| 'subtle'
|
|
||||||
| 'subtle-white'
|
|
||||||
| 'destructive'
|
|
||||||
| 'subtle-destructive'
|
|
||||||
| 'ghosted'
|
|
||||||
| 'ghosted-white'
|
|
||||||
| 'ghosted-destructive';
|
|
||||||
size?: 'small' | 'medium' | 'large' | 'xlarge';
|
size?: 'small' | 'medium' | 'large' | 'xlarge';
|
||||||
fluid?: boolean;
|
fluid?: boolean;
|
||||||
loading?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
|
||||||
({ className, children, variant = 'subtle', size = 'medium', fluid, loading, ...buttonProps }, ref) => {
|
const { className, children, variant = 'subtle', size = 'medium', fluid, ...buttonProps } = props;
|
||||||
return (
|
|
||||||
<button
|
return (
|
||||||
ref={ref}
|
<button
|
||||||
className={cx([
|
ref={ref}
|
||||||
style.baseButton,
|
className={cx([style.baseButton, style[variant], style[size], fluid && style.fluid, className])}
|
||||||
style[variant],
|
type='button'
|
||||||
style[size],
|
{...buttonProps}
|
||||||
fluid && style.fluid,
|
>
|
||||||
loading && style.loading,
|
{children}
|
||||||
className,
|
</button>
|
||||||
])}
|
);
|
||||||
type='button'
|
});
|
||||||
disabled={loading || buttonProps.disabled}
|
|
||||||
{...buttonProps}
|
|
||||||
>
|
|
||||||
<span className={style.content}>{children}</span>
|
|
||||||
{loading && (
|
|
||||||
<div className={style.loadingOverlay}>
|
|
||||||
<IoEllipseOutline className={style.spinner} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Button.displayName = 'Button';
|
Button.displayName = 'Button';
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,7 @@ import { cx } from '../../utils/styleUtils';
|
|||||||
import style from './IconButton.module.scss';
|
import style from './IconButton.module.scss';
|
||||||
|
|
||||||
interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
variant?:
|
variant?: 'primary' | 'subtle' | 'subtle-white' | 'destructive' | 'subtle-destructive' | 'ghosted';
|
||||||
| 'primary'
|
|
||||||
| 'subtle'
|
|
||||||
| 'subtle-white'
|
|
||||||
| 'destructive'
|
|
||||||
| 'subtle-destructive'
|
|
||||||
| 'ghosted'
|
|
||||||
| 'ghosted-white'
|
|
||||||
| 'ghosted-destructive';
|
|
||||||
size?: 'small' | 'medium' | 'large' | 'xlarge';
|
size?: 'small' | 'medium' | 'large' | 'xlarge';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { IoArrowForward } from 'react-icons/io5';
|
import { IoArrowForward } from 'react-icons/io5';
|
||||||
|
import {
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
Select,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
import { navigatorConstants } from '../../../viewerConfig';
|
import { navigatorConstants } from '../../../viewerConfig';
|
||||||
import { setClientRemote } from '../../hooks/useSocket';
|
import { setClientRemote } from '../../hooks/useSocket';
|
||||||
import useUrlPresets from '../../hooks-query/useUrlPresets';
|
import useUrlPresets from '../../hooks-query/useUrlPresets';
|
||||||
import Button from '../buttons/Button';
|
|
||||||
import Info from '../info/Info';
|
import Info from '../info/Info';
|
||||||
import Input from '../input/input/Input';
|
|
||||||
import AppLink from '../link/app-link/AppLink';
|
import AppLink from '../link/app-link/AppLink';
|
||||||
import Modal from '../modal/Modal';
|
|
||||||
import Select from '../select/Select';
|
|
||||||
|
|
||||||
import style from './RedirectClientModal.module.scss';
|
import style from './RedirectClientModal.module.scss';
|
||||||
|
|
||||||
@@ -22,7 +29,8 @@ interface RedirectClientModalProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RedirectClientModal({ id, isOpen, name, currentPath, origin, onClose }: RedirectClientModalProps) {
|
export function RedirectClientModal(props: RedirectClientModalProps) {
|
||||||
|
const { id, isOpen, name, currentPath, origin, onClose } = props;
|
||||||
const { data } = useUrlPresets();
|
const { data } = useUrlPresets();
|
||||||
const [path, setPath] = useState(currentPath);
|
const [path, setPath] = useState(currentPath);
|
||||||
const [selected, setSelected] = useState('/');
|
const [selected, setSelected] = useState('/');
|
||||||
@@ -39,26 +47,13 @@ export function RedirectClientModal({ id, isOpen, name, currentPath, origin, onC
|
|||||||
|
|
||||||
const enabledPresets = data.filter((preset) => preset.enabled);
|
const enabledPresets = data.filter((preset) => preset.enabled);
|
||||||
|
|
||||||
const viewOptions = [
|
|
||||||
...navigatorConstants.map((view) => ({
|
|
||||||
value: `/${view.url}`,
|
|
||||||
label: view.label,
|
|
||||||
})),
|
|
||||||
...enabledPresets.map((preset) => ({
|
|
||||||
value: preset.pathAndParams,
|
|
||||||
label: `Preset: ${preset.alias}`,
|
|
||||||
})),
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal isOpen={isOpen} onClose={onClose} variant='ontime'>
|
||||||
isOpen={isOpen}
|
<ModalOverlay />
|
||||||
onClose={onClose}
|
<ModalContent maxWidth='max(480px, 35vw)'>
|
||||||
showCloseButton
|
<ModalHeader>Redirect: {name}</ModalHeader>
|
||||||
showBackdrop
|
<ModalCloseButton />
|
||||||
title={`Redirect: ${name}`}
|
<ModalBody>
|
||||||
bodyElements={
|
|
||||||
<>
|
|
||||||
<Info>
|
<Info>
|
||||||
Remotely redirect the client to a different URL. <br />
|
Remotely redirect the client to a different URL. <br />
|
||||||
Either by selecting a URL Preset or entering a custom path.
|
Either by selecting a URL Preset or entering a custom path.
|
||||||
@@ -70,21 +65,36 @@ export function RedirectClientModal({ id, isOpen, name, currentPath, origin, onC
|
|||||||
<span className={style.label}>Select View or URL Preset</span>
|
<span className={style.label}>Select View or URL Preset</span>
|
||||||
<div className={style.textEntry}>
|
<div className={style.textEntry}>
|
||||||
<Select
|
<Select
|
||||||
fluid
|
size='md'
|
||||||
options={viewOptions}
|
variant='ontime'
|
||||||
defaultValue={viewOptions[0].value}
|
isDisabled={enabledPresets.length === 0}
|
||||||
onValueChange={(value) => setSelected(value)}
|
onChange={(event) => setSelected(event.target.value)}
|
||||||
disabled={enabledPresets.length === 0}
|
>
|
||||||
/>
|
<option value='/'>Select view or preset</option>
|
||||||
<Button
|
{navigatorConstants.map((view) => {
|
||||||
variant='primary'
|
return (
|
||||||
|
<option key={view.url} value={`/${view.url}`}>
|
||||||
|
{view.label}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{enabledPresets.map((preset) => {
|
||||||
|
return (
|
||||||
|
<option key={preset.pathAndParams} value={preset.pathAndParams}>
|
||||||
|
{`Preset: ${preset.alias}`}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
<IconButton
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='md'
|
||||||
aria-label='Redirect to preset'
|
aria-label='Redirect to preset'
|
||||||
className={style.redirect}
|
className={style.redirect}
|
||||||
disabled={enabledPresets.length === 0 || selected === '/'}
|
icon={<IoArrowForward />}
|
||||||
|
isDisabled={enabledPresets.length === 0 || selected === '/'}
|
||||||
onClick={() => handleRedirect(selected)}
|
onClick={() => handleRedirect(selected)}
|
||||||
>
|
/>
|
||||||
Redirect <IoArrowForward />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.inlineEntry}>
|
<div className={style.inlineEntry}>
|
||||||
@@ -92,24 +102,25 @@ export function RedirectClientModal({ id, isOpen, name, currentPath, origin, onC
|
|||||||
<label className={style.textEntry}>
|
<label className={style.textEntry}>
|
||||||
{origin}
|
{origin}
|
||||||
<Input
|
<Input
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='md'
|
||||||
placeholder='eg. /minimal?key=0000ffff'
|
placeholder='eg. /minimal?key=0000ffff'
|
||||||
fluid
|
|
||||||
value={path}
|
value={path}
|
||||||
onChange={(event) => setPath(event.target.value)}
|
onChange={(event) => setPath(event.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<Button
|
<IconButton
|
||||||
variant='primary'
|
variant='ontime-filled'
|
||||||
|
size='md'
|
||||||
aria-label='Redirect'
|
aria-label='Redirect'
|
||||||
disabled={path === currentPath || path === ''}
|
isDisabled={path === currentPath || path === ''}
|
||||||
className={style.redirect}
|
className={style.redirect}
|
||||||
|
icon={<IoArrowForward />}
|
||||||
onClick={() => handleRedirect(path)}
|
onClick={() => handleRedirect(path)}
|
||||||
>
|
/>
|
||||||
Redirect <IoArrowForward />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</ModalBody>
|
||||||
}
|
</ModalContent>
|
||||||
/>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
color: $ui-white;
|
color: $ui-white;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
box-shadow: $box-shadow-l1;
|
box-shadow: $box-shadow-l1;
|
||||||
border: 1px solid $gray-1100;
|
border: 1px solid $gray-1200;
|
||||||
}
|
}
|
||||||
|
|
||||||
.backdrop {
|
.backdrop {
|
||||||
|
|||||||
@@ -7,29 +7,15 @@
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-size: $text-body-size;
|
font-size: $text-body-size;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
color: $gray-200;
|
|
||||||
|
.content {
|
||||||
|
color: $gray-200;
|
||||||
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
min-width: 1.5rem;
|
min-width: 1.5rem;
|
||||||
align-self: start;
|
align-self: start;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
svg {
|
|
||||||
color: $info-blue;
|
color: $info-blue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.warning {
|
|
||||||
svg {
|
|
||||||
color: $orange-500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
svg {
|
|
||||||
color: $red-500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
import { IoAlertCircle, IoWarning } from 'react-icons/io5';
|
import { IoAlertCircle } from 'react-icons/io5';
|
||||||
|
|
||||||
import { cx } from '../../utils/styleUtils';
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|
||||||
@@ -7,15 +7,14 @@ import style from './Info.module.scss';
|
|||||||
|
|
||||||
interface InfoProps {
|
interface InfoProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
type?: 'info' | 'warning' | 'error';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Info({ className, type = 'info', children }: PropsWithChildren<InfoProps>) {
|
export default function Info(props: PropsWithChildren<InfoProps>) {
|
||||||
|
const { className, children } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx([style.infoLabel, style[type], className])}>
|
<div className={cx([style.infoLabel, className])}>
|
||||||
{type === 'info' && <IoAlertCircle />}
|
<IoAlertCircle />
|
||||||
{type === 'warning' && <IoWarning />}
|
|
||||||
{type === 'error' && <IoWarning />}
|
|
||||||
<div>{children}</div>
|
<div>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
@use '../../../../theme/viewerDefs' as *;
|
|
||||||
|
|
||||||
.textarea {
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
display: block;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 400;
|
|
||||||
color: $gray-200;
|
|
||||||
border-radius: $component-border-radius-md;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
|
|
||||||
padding-inline: 0.5em;
|
|
||||||
outline: none;
|
|
||||||
|
|
||||||
&:hover:not(:disabled) {
|
|
||||||
background-color: $gray-1100;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus:not(:read-only) {
|
|
||||||
background-color: $gray-1000;
|
|
||||||
border: 1px solid $blue-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: 0.4;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: $gray-500;
|
|
||||||
letter-spacing: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtle {
|
|
||||||
background-color: $gray-1200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ghosted {
|
|
||||||
background-color: transparent;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fluid {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { forwardRef, TextareaHTMLAttributes } from 'react';
|
|
||||||
|
|
||||||
import { cx } from '../../../utils/styleUtils';
|
|
||||||
|
|
||||||
import style from './Textarea.module.scss';
|
|
||||||
|
|
||||||
export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
||||||
variant?: 'subtle' | 'ghosted';
|
|
||||||
fluid?: boolean;
|
|
||||||
resize?: 'none' | 'both' | 'horizontal' | 'vertical';
|
|
||||||
}
|
|
||||||
|
|
||||||
const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function TextArea(
|
|
||||||
{ className, variant = 'subtle', fluid, rows = 5, resize = 'none', style: customStyle, ...textareaProps },
|
|
||||||
ref,
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<textarea
|
|
||||||
ref={ref}
|
|
||||||
autoCorrect='off'
|
|
||||||
autoComplete='off'
|
|
||||||
spellCheck='false'
|
|
||||||
rows={rows}
|
|
||||||
style={{ ...customStyle, resize }}
|
|
||||||
className={cx([style.textarea, style[variant], fluid && style.fluid, className])}
|
|
||||||
{...textareaProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Textarea;
|
|
||||||
@@ -1,24 +1,26 @@
|
|||||||
.modal {
|
.modal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 10vh;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
|
||||||
|
z-index: $zindex-dialog;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
padding-inline: 1rem;
|
padding-inline: 1rem;
|
||||||
min-width: min(680px, 90vw);
|
min-width: min(680px, 90vw);
|
||||||
min-height: min(200px, 10vh);
|
min-height: min(200px, 10vh);
|
||||||
max-width: min(680px, 90vw);
|
|
||||||
|
|
||||||
background-color: $gray-1250;
|
background-color: $gray-1250;
|
||||||
color: $ui-white;
|
color: $ui-white;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
box-shadow: $box-shadow-l1;
|
box-shadow: $box-shadow-l1;
|
||||||
border: 1px solid $gray-1100;
|
border: 1px solid $gray-1200;
|
||||||
}
|
}
|
||||||
|
|
||||||
.backdrop {
|
.backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
z-index: $zindex-backdrop;
|
||||||
background-color: $backdrop-color;
|
background-color: $backdrop-color;
|
||||||
transition: opacity 300ms cubic-bezier(0.45, 1.005, 0, 1.005);
|
transition: opacity 300ms cubic-bezier(0.45, 1.005, 0, 1.005);
|
||||||
|
|
||||||
@@ -43,7 +45,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
max-height: 60vh;
|
max-height: min(80vh, 600px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import style from './Modal.module.scss';
|
|||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
title?: string;
|
title: string;
|
||||||
showCloseButton?: boolean;
|
showCloseButton?: boolean;
|
||||||
showBackdrop?: boolean;
|
showBackdrop?: boolean;
|
||||||
bodyElements: ReactNode;
|
bodyElements: ReactNode;
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
|
|
||||||
background-color: $gray-1250;
|
background-color: $gray-1250;
|
||||||
color: $ui-white;
|
color: $ui-white;
|
||||||
border-right: 1px solid $gray-1100;
|
border-right: 1px solid $gray-1200;
|
||||||
|
|
||||||
&[data-open] {
|
&[data-open] {
|
||||||
transform: translateX(0%);
|
transform: translateX(0%);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useDisclosure, useHotkeys } from '@mantine/hooks';
|
import { useDisclosure } from '@chakra-ui/react';
|
||||||
|
import { useHotkeys } from '@mantine/hooks';
|
||||||
|
|
||||||
import FloatingNavigation from './floating-navigation/FloatingNavigation';
|
import FloatingNavigation from './floating-navigation/FloatingNavigation';
|
||||||
import ViewLockedIcon from './view-locked-icon/ViewLockedIcon';
|
import ViewLockedIcon from './view-locked-icon/ViewLockedIcon';
|
||||||
@@ -13,15 +14,17 @@ interface ViewNavigationMenuProps {
|
|||||||
|
|
||||||
export default memo(ViewNavigationMenu);
|
export default memo(ViewNavigationMenu);
|
||||||
function ViewNavigationMenu({ isLockable, supressSettings }: ViewNavigationMenuProps) {
|
function ViewNavigationMenu({ isLockable, supressSettings }: ViewNavigationMenuProps) {
|
||||||
const [isMenuOpen, menuHandler] = useDisclosure();
|
const { isOpen: isMenuOpen, onOpen: onMenuOpen, onClose: onMenuClose } = useDisclosure();
|
||||||
const { showEditFormDrawer, isViewLocked } = useViewEditor({ isLockable });
|
const { showEditFormDrawer, isViewLocked } = useViewEditor({ isLockable });
|
||||||
|
|
||||||
|
const toggleMenu = () => (isMenuOpen ? onMenuClose() : onMenuOpen());
|
||||||
|
|
||||||
useHotkeys([
|
useHotkeys([
|
||||||
[
|
[
|
||||||
'Space',
|
'Space',
|
||||||
() => {
|
() => {
|
||||||
if (isViewLocked) return;
|
if (isViewLocked) return;
|
||||||
menuHandler.toggle();
|
toggleMenu();
|
||||||
},
|
},
|
||||||
{ preventDefault: true },
|
{ preventDefault: true },
|
||||||
],
|
],
|
||||||
@@ -42,10 +45,10 @@ function ViewNavigationMenu({ isLockable, supressSettings }: ViewNavigationMenuP
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FloatingNavigation
|
<FloatingNavigation
|
||||||
toggleMenu={menuHandler.toggle}
|
toggleMenu={toggleMenu}
|
||||||
toggleSettings={supressSettings ? undefined : () => showEditFormDrawer()}
|
toggleSettings={supressSettings ? undefined : () => showEditFormDrawer()}
|
||||||
/>
|
/>
|
||||||
<NavigationMenu isOpen={isMenuOpen} onClose={menuHandler.close} />
|
<NavigationMenu isOpen={isMenuOpen} onClose={onMenuClose} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -6,9 +6,9 @@ import NavigationMenuItem from '../navigation-menu-item/NavigationMenuItem';
|
|||||||
|
|
||||||
export default function EditorNavigation() {
|
export default function EditorNavigation() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const isSmallDevice = useIsSmallDevice();
|
const isSmallDevide = useIsSmallDevice();
|
||||||
|
|
||||||
if (!isSmallDevice) {
|
if (!isSmallDevide) {
|
||||||
return (
|
return (
|
||||||
<NavigationMenuItem active={location.pathname === '/editor'} onClick={() => navigate('/editor')}>
|
<NavigationMenuItem active={location.pathname === '/editor'} onClick={() => navigate('/editor')}>
|
||||||
<IoLockClosedOutline />
|
<IoLockClosedOutline />
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { IoPause, IoPlay, IoStop } from 'react-icons/io5';
|
||||||
|
import { Tooltip } from '@chakra-ui/react';
|
||||||
|
import { Playback } from 'ontime-types';
|
||||||
|
|
||||||
|
import { tooltipDelayFast } from '../../../ontimeConfig';
|
||||||
|
|
||||||
|
interface PlaybackIconProps {
|
||||||
|
state: Playback;
|
||||||
|
skipTooltip?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PlaybackIcon(props: PlaybackIconProps) {
|
||||||
|
const { state, skipTooltip, className } = props;
|
||||||
|
|
||||||
|
// if timer is Pause or Armed
|
||||||
|
let label = 'Timer Paused';
|
||||||
|
let Icon = IoPause;
|
||||||
|
|
||||||
|
if (state === Playback.Roll) {
|
||||||
|
label = 'Timer Rolling';
|
||||||
|
Icon = IoPlay;
|
||||||
|
} else if (state === Playback.Play) {
|
||||||
|
label = 'Timer Playing';
|
||||||
|
Icon = IoPlay;
|
||||||
|
} else if (state === Playback.Stop) {
|
||||||
|
label = 'Timer Stopped';
|
||||||
|
Icon = IoStop;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skipTooltip) {
|
||||||
|
return <Icon className={className} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip openDelay={tooltipDelayFast} label={label} shouldWrapChildren>
|
||||||
|
<Icon className={className} />
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -17,11 +17,11 @@
|
|||||||
font-size: calc(1rem - 2px);
|
font-size: calc(1rem - 2px);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
&:hover:not([data-disabled]) {
|
&:hover:not(:disabled) {
|
||||||
background-color: $gray-1100;
|
background-color: $gray-1100;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active:not([data-disabled]) {
|
&:active {
|
||||||
background-color: $gray-1000;
|
background-color: $gray-1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,14 +29,10 @@
|
|||||||
background-color: $gray-1000;
|
background-color: $gray-1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-disabled] {
|
&:disabled {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.fluid {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.selectIcon {
|
.selectIcon {
|
||||||
|
|||||||
@@ -2,24 +2,31 @@ import { IoCheckmark } from 'react-icons/io5';
|
|||||||
import { LuChevronsUpDown } from 'react-icons/lu';
|
import { LuChevronsUpDown } from 'react-icons/lu';
|
||||||
import { Select as BaseSelect } from '@base-ui-components/react/select';
|
import { Select as BaseSelect } from '@base-ui-components/react/select';
|
||||||
|
|
||||||
import { cx } from '../../utils/styleUtils';
|
|
||||||
|
|
||||||
import styles from './Select.module.scss';
|
import styles from './Select.module.scss';
|
||||||
|
|
||||||
interface SelectProps<T> extends Omit<BaseSelect.Root.Props<T>, 'items'> {
|
interface SelectProps<T extends string | null = string> {
|
||||||
// overload items to not allow undefined values
|
defaultValue?: T;
|
||||||
options: {
|
options: {
|
||||||
value: T;
|
value: NonNullable<T>;
|
||||||
label: string;
|
label: string;
|
||||||
|
disabled?: boolean; // exposed to allow creating a non-selectable option
|
||||||
}[];
|
}[];
|
||||||
fluid?: boolean;
|
placeholder?: string;
|
||||||
|
value?: T;
|
||||||
|
onChange?: (value: NonNullable<T>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Select<T>({ options, fluid, ...selectRootProps }: SelectProps<T>) {
|
export default function Select<T extends string | null = string>({
|
||||||
|
defaultValue,
|
||||||
|
options,
|
||||||
|
placeholder,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: SelectProps<T>) {
|
||||||
return (
|
return (
|
||||||
<BaseSelect.Root items={options} {...selectRootProps}>
|
<BaseSelect.Root defaultValue={defaultValue} onValueChange={onChange} value={value}>
|
||||||
<BaseSelect.Trigger className={cx([styles.select, fluid && styles.fluid])}>
|
<BaseSelect.Trigger className={styles.select}>
|
||||||
<BaseSelect.Value />
|
<BaseSelect.Value placeholder={placeholder} />
|
||||||
<BaseSelect.Icon className={styles.selectIcon}>
|
<BaseSelect.Icon className={styles.selectIcon}>
|
||||||
<LuChevronsUpDown />
|
<LuChevronsUpDown />
|
||||||
</BaseSelect.Icon>
|
</BaseSelect.Icon>
|
||||||
@@ -28,14 +35,16 @@ export default function Select<T>({ options, fluid, ...selectRootProps }: Select
|
|||||||
<BaseSelect.Positioner side='bottom' align='start'>
|
<BaseSelect.Positioner side='bottom' align='start'>
|
||||||
<BaseSelect.ScrollUpArrow className={styles.scrollArrow} />
|
<BaseSelect.ScrollUpArrow className={styles.scrollArrow} />
|
||||||
<BaseSelect.Popup className={styles.popup}>
|
<BaseSelect.Popup className={styles.popup}>
|
||||||
{options.map(({ label, value }) => (
|
{options.map((option) => {
|
||||||
<BaseSelect.Item key={String(value)} className={styles.item} value={value}>
|
return (
|
||||||
<BaseSelect.ItemIndicator className={styles.itemIndicator}>
|
<BaseSelect.Item key={option.value} className={styles.item} value={option.value}>
|
||||||
<IoCheckmark className={styles.itemIndicatorIcon} />
|
<BaseSelect.ItemIndicator className={styles.itemIndicator}>
|
||||||
</BaseSelect.ItemIndicator>
|
<IoCheckmark className={styles.itemIndicatorIcon} />
|
||||||
<BaseSelect.ItemText className={styles.itemLabel}>{label}</BaseSelect.ItemText>
|
</BaseSelect.ItemIndicator>
|
||||||
</BaseSelect.Item>
|
<BaseSelect.ItemText className={styles.itemLabel}>{option.label}</BaseSelect.ItemText>
|
||||||
))}
|
</BaseSelect.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</BaseSelect.Popup>
|
</BaseSelect.Popup>
|
||||||
<BaseSelect.ScrollDownArrow className={styles.scrollArrow} />
|
<BaseSelect.ScrollDownArrow className={styles.scrollArrow} />
|
||||||
</BaseSelect.Positioner>
|
</BaseSelect.Positioner>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
background-color: $gray-1250;
|
background-color: $gray-1250;
|
||||||
color: $ui-white;
|
color: $ui-white;
|
||||||
border-left: 1px solid $gray-1100;
|
border-left: 1px solid $gray-1200;
|
||||||
|
|
||||||
&[data-open] {
|
&[data-open] {
|
||||||
transform: translateX(0%);
|
transform: translateX(0%);
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
// CustomFields record type is no longer used here
|
import { CustomFields } from 'ontime-types';
|
||||||
|
|
||||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||||
import { CUSTOM_FIELDS } from '../api/constants';
|
import { CUSTOM_FIELDS } from '../api/constants';
|
||||||
import { getCustomFields, CustomFieldWithKey } from '../api/customFields'; // Import CustomFieldWithKey
|
import { getCustomFields } from '../api/customFields';
|
||||||
|
|
||||||
const placeholder: CustomFieldWithKey[] = []; // Placeholder is now an empty array
|
const placeholder: CustomFields = {};
|
||||||
|
|
||||||
export default function useCustomFields() {
|
export default function useCustomFields() {
|
||||||
// Explicitly type the useQuery hook
|
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||||
const { data, status, isFetching, isError, refetch } = useQuery<CustomFieldWithKey[], Error>({
|
|
||||||
queryKey: CUSTOM_FIELDS,
|
queryKey: CUSTOM_FIELDS,
|
||||||
queryFn: getCustomFields,
|
queryFn: getCustomFields,
|
||||||
placeholderData: (previousData, _previousQuery) => previousData ?? placeholder,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
retry: 5,
|
retry: 5,
|
||||||
retryDelay: (attempt) => attempt * 2500,
|
retryDelay: (attempt) => attempt * 2500,
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { ProjectRundownsList } from 'ontime-types';
|
|
||||||
|
|
||||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
|
||||||
import { PROJECT_RUNDOWNS } from '../api/constants';
|
|
||||||
import { fetchProjectRundownList } from '../api/rundown';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Project rundowns
|
|
||||||
*/
|
|
||||||
export function useProjectRundowns() {
|
|
||||||
const { data, status, isError, refetch, isFetching } = useQuery<ProjectRundownsList>({
|
|
||||||
queryKey: PROJECT_RUNDOWNS,
|
|
||||||
queryFn: fetchProjectRundownList,
|
|
||||||
placeholderData: (previousData, _previousQuery) => previousData,
|
|
||||||
retry: 5,
|
|
||||||
retryDelay: (attempt) => attempt * 2500,
|
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
|
||||||
networkMode: 'always',
|
|
||||||
});
|
|
||||||
return { data: data ?? { loaded: '', rundowns: [] }, status, isError, refetch, isFetching };
|
|
||||||
}
|
|
||||||
@@ -4,11 +4,7 @@ import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
|||||||
import { URL_PRESETS } from '../api/constants';
|
import { URL_PRESETS } from '../api/constants';
|
||||||
import { getUrlPresets } from '../api/urlPresets';
|
import { getUrlPresets } from '../api/urlPresets';
|
||||||
|
|
||||||
interface FetchProps {
|
export default function useUrlPresets() {
|
||||||
skip?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function useUrlPresets({ skip = false }: FetchProps = {}) {
|
|
||||||
const { data, status, isError, refetch } = useQuery({
|
const { data, status, isError, refetch } = useQuery({
|
||||||
queryKey: URL_PRESETS,
|
queryKey: URL_PRESETS,
|
||||||
queryFn: getUrlPresets,
|
queryFn: getUrlPresets,
|
||||||
@@ -17,7 +13,6 @@ export default function useUrlPresets({ skip = false }: FetchProps = {}) {
|
|||||||
retryDelay: (attempt) => attempt * 2500,
|
retryDelay: (attempt) => attempt * 2500,
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
networkMode: 'always',
|
networkMode: 'always',
|
||||||
enabled: !skip,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return { data: data ?? [], status, isError, refetch };
|
return { data: data ?? [], status, isError, refetch };
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export const useEntryActions = () => {
|
|||||||
* Calls mutation to add new entry
|
* Calls mutation to add new entry
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
const { mutateAsync: addEntryMutation } = useMutation({
|
const _addEntryMutation = useMutation({
|
||||||
// TODO(v4): optimistic create entry
|
// TODO(v4): optimistic create entry
|
||||||
mutationFn: postAddEntry,
|
mutationFn: postAddEntry,
|
||||||
onSettled: () => queryClient.invalidateQueries({ queryKey: RUNDOWN }),
|
onSettled: () => queryClient.invalidateQueries({ queryKey: RUNDOWN }),
|
||||||
@@ -143,13 +143,13 @@ export const useEntryActions = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await addEntryMutation(newEntry);
|
await _addEntryMutation.mutateAsync(newEntry);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logAxiosError('Failed adding event', error);
|
logAxiosError('Failed adding event', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
addEntryMutation,
|
_addEntryMutation,
|
||||||
defaultDangerTime,
|
defaultDangerTime,
|
||||||
defaultDuration,
|
defaultDuration,
|
||||||
defaultEndAction,
|
defaultEndAction,
|
||||||
@@ -165,7 +165,7 @@ export const useEntryActions = () => {
|
|||||||
* Calls mutation to clone a selection
|
* Calls mutation to clone a selection
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
const { mutateAsync: cloneEntryMutation } = useMutation({
|
const _cloneMutation = useMutation({
|
||||||
mutationFn: postCloneEntry,
|
mutationFn: postCloneEntry,
|
||||||
onSettled: () => queryClient.invalidateQueries({ queryKey: RUNDOWN }),
|
onSettled: () => queryClient.invalidateQueries({ queryKey: RUNDOWN }),
|
||||||
});
|
});
|
||||||
@@ -176,19 +176,19 @@ export const useEntryActions = () => {
|
|||||||
const clone = useCallback(
|
const clone = useCallback(
|
||||||
async (entryId: EntryId) => {
|
async (entryId: EntryId) => {
|
||||||
try {
|
try {
|
||||||
await cloneEntryMutation(entryId);
|
await _cloneMutation.mutateAsync(entryId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logAxiosError('Error cloning entry', error);
|
logAxiosError('Error cloning entry', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[cloneEntryMutation],
|
[_cloneMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls mutation to update existing entry
|
* Calls mutation to update existing entry
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
const { mutateAsync: updateEntryMutation } = useMutation({
|
const _updateEntryMutation = useMutation({
|
||||||
mutationFn: putEditEntry,
|
mutationFn: putEditEntry,
|
||||||
// we optimistically update here
|
// we optimistically update here
|
||||||
onMutate: async (newEvent) => {
|
onMutate: async (newEvent) => {
|
||||||
@@ -234,12 +234,12 @@ export const useEntryActions = () => {
|
|||||||
const updateEntry = useCallback(
|
const updateEntry = useCallback(
|
||||||
async (event: Partial<OntimeEntry>) => {
|
async (event: Partial<OntimeEntry>) => {
|
||||||
try {
|
try {
|
||||||
await updateEntryMutation(event);
|
await _updateEntryMutation.mutateAsync(event);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logAxiosError('Error updating event', error);
|
logAxiosError('Error updating event', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[updateEntryMutation],
|
[_updateEntryMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateCustomField = useCallback(
|
const updateCustomField = useCallback(
|
||||||
@@ -287,7 +287,7 @@ export const useEntryActions = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateEntryMutation(newEvent);
|
await _updateEntryMutation.mutateAsync(newEvent);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logAxiosError('Error updating event', error);
|
logAxiosError('Error updating event', error);
|
||||||
}
|
}
|
||||||
@@ -339,14 +339,14 @@ export const useEntryActions = () => {
|
|||||||
return previousEnd;
|
return previousEnd;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[updateEntryMutation, queryClient],
|
[_updateEntryMutation, queryClient],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls mutation to edit multiple events
|
* Calls mutation to edit multiple events
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
const { mutateAsync: batchUpdateEventsMutation } = useMutation({
|
const _batchUpdateEventsMutation = useMutation({
|
||||||
mutationFn: putBatchEditEvents,
|
mutationFn: putBatchEditEvents,
|
||||||
onMutate: async ({ ids, data }) => {
|
onMutate: async ({ ids, data }) => {
|
||||||
// cancel ongoing queries
|
// cancel ongoing queries
|
||||||
@@ -405,19 +405,19 @@ export const useEntryActions = () => {
|
|||||||
const batchUpdateEvents = useCallback(
|
const batchUpdateEvents = useCallback(
|
||||||
async (data: Partial<OntimeEvent>, eventIds: string[]) => {
|
async (data: Partial<OntimeEvent>, eventIds: string[]) => {
|
||||||
try {
|
try {
|
||||||
await batchUpdateEventsMutation({ ids: eventIds, data });
|
await _batchUpdateEventsMutation.mutateAsync({ ids: eventIds, data });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logAxiosError('Error updating events', error);
|
logAxiosError('Error updating events', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[batchUpdateEventsMutation],
|
[_batchUpdateEventsMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls mutation to delete an entry
|
* Calls mutation to delete an entry
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
const { mutateAsync: deleteEntryMutation } = useMutation({
|
const _deleteEntryMutation = useMutation({
|
||||||
mutationFn: deleteEntries,
|
mutationFn: deleteEntries,
|
||||||
// we optimistically update here
|
// we optimistically update here
|
||||||
onMutate: async (entryIds: EntryId[]) => {
|
onMutate: async (entryIds: EntryId[]) => {
|
||||||
@@ -462,19 +462,19 @@ export const useEntryActions = () => {
|
|||||||
const deleteEntry = useCallback(
|
const deleteEntry = useCallback(
|
||||||
async (entryIds: EntryId[]) => {
|
async (entryIds: EntryId[]) => {
|
||||||
try {
|
try {
|
||||||
await deleteEntryMutation(entryIds);
|
await _deleteEntryMutation.mutateAsync(entryIds);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logAxiosError('Error deleting event', error);
|
logAxiosError('Error deleting event', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[deleteEntryMutation],
|
[_deleteEntryMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls mutation to delete all events
|
* Calls mutation to delete all events
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
const { mutateAsync: deleteAllEntriesMutation } = useMutation({
|
const _deleteAllEntriesMutation = useMutation({
|
||||||
mutationFn: requestDeleteAll,
|
mutationFn: requestDeleteAll,
|
||||||
// we optimistically update here
|
// we optimistically update here
|
||||||
onMutate: async () => {
|
onMutate: async () => {
|
||||||
@@ -514,17 +514,17 @@ export const useEntryActions = () => {
|
|||||||
*/
|
*/
|
||||||
const deleteAllEntries = useCallback(async () => {
|
const deleteAllEntries = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
await deleteAllEntriesMutation();
|
await _deleteAllEntriesMutation.mutateAsync();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logAxiosError('Error deleting events', error);
|
logAxiosError('Error deleting events', error);
|
||||||
}
|
}
|
||||||
}, [deleteAllEntriesMutation]);
|
}, [_deleteAllEntriesMutation]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls mutation to apply a delay
|
* Calls mutation to apply a delay
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
const { mutateAsync: applyDelayMutation } = useMutation({
|
const _applyDelayMutation = useMutation({
|
||||||
mutationFn: requestApplyDelay,
|
mutationFn: requestApplyDelay,
|
||||||
onSuccess: (response) => {
|
onSuccess: (response) => {
|
||||||
if (!response.data) return;
|
if (!response.data) return;
|
||||||
@@ -551,19 +551,19 @@ export const useEntryActions = () => {
|
|||||||
const applyDelay = useCallback(
|
const applyDelay = useCallback(
|
||||||
async (delayEventId: EntryId) => {
|
async (delayEventId: EntryId) => {
|
||||||
try {
|
try {
|
||||||
await applyDelayMutation(delayEventId);
|
await _applyDelayMutation.mutateAsync(delayEventId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logAxiosError('Error applying delay', error);
|
logAxiosError('Error applying delay', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[applyDelayMutation],
|
[_applyDelayMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls mutation to dissolve a block
|
* Calls mutation to dissolve a block
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
const { mutateAsync: ungroupMutation } = useMutation({
|
const _ungroupMutation = useMutation({
|
||||||
mutationFn: requestUngroup,
|
mutationFn: requestUngroup,
|
||||||
onSuccess: (response) => {
|
onSuccess: (response) => {
|
||||||
if (!response.data) return;
|
if (!response.data) return;
|
||||||
@@ -587,19 +587,19 @@ export const useEntryActions = () => {
|
|||||||
const ungroup = useCallback(
|
const ungroup = useCallback(
|
||||||
async (blockId: EntryId) => {
|
async (blockId: EntryId) => {
|
||||||
try {
|
try {
|
||||||
await ungroupMutation(blockId);
|
await _ungroupMutation.mutateAsync(blockId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logAxiosError('Error dissolving block', error);
|
logAxiosError('Error dissolving block', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[ungroupMutation],
|
[_ungroupMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls mutation to create a block with a selection
|
* Calls mutation to create a block with a selection
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
const { mutateAsync: groupEntriesMutation } = useMutation({
|
const _groupEntriesMutation = useMutation({
|
||||||
mutationFn: requestGroupEntries,
|
mutationFn: requestGroupEntries,
|
||||||
onSuccess: (response) => {
|
onSuccess: (response) => {
|
||||||
if (!response.data) return;
|
if (!response.data) return;
|
||||||
@@ -623,19 +623,19 @@ export const useEntryActions = () => {
|
|||||||
const groupEntries = useCallback(
|
const groupEntries = useCallback(
|
||||||
async (entryIds: EntryId[]) => {
|
async (entryIds: EntryId[]) => {
|
||||||
try {
|
try {
|
||||||
await groupEntriesMutation(entryIds);
|
await _groupEntriesMutation.mutateAsync(entryIds);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logAxiosError('Error grouping entries', error);
|
logAxiosError('Error grouping entries', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[groupEntriesMutation],
|
[_groupEntriesMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls mutation to reorder an entry
|
* Calls mutation to reorder an entry
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
const { mutateAsync: reorderEntryMutation } = useMutation({
|
const _reorderEntryMutation = useMutation({
|
||||||
mutationFn: patchReorderEntry,
|
mutationFn: patchReorderEntry,
|
||||||
// Mutation finished, failed or successful
|
// Mutation finished, failed or successful
|
||||||
// Fetch anyway, just to be sure
|
// Fetch anyway, just to be sure
|
||||||
@@ -644,40 +644,6 @@ export const useEntryActions = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Reorders a given entry one step up or down in the timeline
|
|
||||||
*/
|
|
||||||
const move = useCallback(
|
|
||||||
async (entryId: EntryId, direction: 'up' | 'down') => {
|
|
||||||
const rundown = queryClient.getQueryData<Rundown>(RUNDOWN);
|
|
||||||
if (!rundown) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { destinationId, order } =
|
|
||||||
direction === 'up'
|
|
||||||
? moveUp(entryId, rundown.flatOrder, rundown.entries)
|
|
||||||
: moveDown(entryId, rundown.flatOrder, rundown.entries);
|
|
||||||
|
|
||||||
if (!destinationId) {
|
|
||||||
return; // noop
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const reorderObject: ReorderEntry = {
|
|
||||||
entryId,
|
|
||||||
destinationId,
|
|
||||||
order,
|
|
||||||
};
|
|
||||||
await reorderEntryMutation(reorderObject);
|
|
||||||
} catch (error) {
|
|
||||||
logAxiosError('Error re-ordering event', error);
|
|
||||||
}
|
|
||||||
// the rundown needs to know whether we moved into a block
|
|
||||||
return rundown.entries[destinationId]?.type === 'block' ? destinationId : undefined;
|
|
||||||
},
|
|
||||||
[queryClient, reorderEntryMutation],
|
|
||||||
);
|
|
||||||
/**
|
/**
|
||||||
* Reorders a given entry
|
* Reorders a given entry
|
||||||
*/
|
*/
|
||||||
@@ -689,19 +655,43 @@ export const useEntryActions = () => {
|
|||||||
destinationId,
|
destinationId,
|
||||||
order,
|
order,
|
||||||
};
|
};
|
||||||
await reorderEntryMutation(reorderObject);
|
await _reorderEntryMutation.mutateAsync(reorderObject);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logAxiosError('Error re-ordering event', error);
|
logAxiosError('Error re-ordering event', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[reorderEntryMutation],
|
[_reorderEntryMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const move = useCallback(async (entryId: EntryId, direction: 'up' | 'down') => {
|
||||||
|
const cachedRundown = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||||
|
if (!cachedRundown?.order) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { destinationId, order } =
|
||||||
|
direction === 'up'
|
||||||
|
? moveUp(entryId, cachedRundown.order, cachedRundown.entries)
|
||||||
|
: moveDown(entryId, cachedRundown.order, cachedRundown.entries);
|
||||||
|
|
||||||
|
if (destinationId) {
|
||||||
|
try {
|
||||||
|
const reorderObject: ReorderEntry = {
|
||||||
|
entryId,
|
||||||
|
destinationId,
|
||||||
|
order: order as 'before' | 'after' | 'insert',
|
||||||
|
};
|
||||||
|
await _reorderEntryMutation.mutateAsync(reorderObject);
|
||||||
|
} catch (error) {
|
||||||
|
logAxiosError('Error re-ordering event', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls mutation to swap events
|
* Calls mutation to swap events
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
const { mutateAsync: swapEventsMutation } = useMutation({
|
const _swapEvents = useMutation({
|
||||||
mutationFn: requestEventSwap,
|
mutationFn: requestEventSwap,
|
||||||
// we optimistically update here
|
// we optimistically update here
|
||||||
onMutate: async ({ from, to }) => {
|
onMutate: async ({ from, to }) => {
|
||||||
@@ -755,12 +745,12 @@ export const useEntryActions = () => {
|
|||||||
const swapEvents = useCallback(
|
const swapEvents = useCallback(
|
||||||
async ({ from, to }: SwapEntry) => {
|
async ({ from, to }: SwapEntry) => {
|
||||||
try {
|
try {
|
||||||
await swapEventsMutation({ from, to });
|
await _swapEvents.mutateAsync({ from, to });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logAxiosError('Error re-ordering event', error);
|
logAxiosError('Error re-ordering event', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[swapEventsMutation],
|
[_swapEvents],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { MutableRefObject, useCallback, useEffect, useRef } from 'react';
|
import { MutableRefObject, useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
import { useSelectedEventId } from './useSocket';
|
|
||||||
|
|
||||||
function scrollToComponent<ComponentRef extends HTMLElement, ScrollRef extends HTMLElement>(
|
function scrollToComponent<ComponentRef extends HTMLElement, ScrollRef extends HTMLElement>(
|
||||||
componentRef: MutableRefObject<ComponentRef>,
|
componentRef: MutableRefObject<ComponentRef>,
|
||||||
@@ -18,23 +16,6 @@ function scrollToComponent<ComponentRef extends HTMLElement, ScrollRef extends H
|
|||||||
scrollRef.current.scrollTo({ top, behavior: 'smooth' });
|
scrollRef.current.scrollTo({ top, behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
|
|
||||||
function snapToComponent<ComponentRef extends HTMLElement, ScrollRef extends HTMLElement>(
|
|
||||||
componentRef: MutableRefObject<ComponentRef>,
|
|
||||||
scrollRef: MutableRefObject<ScrollRef>,
|
|
||||||
topOffset: number,
|
|
||||||
) {
|
|
||||||
if (!componentRef.current || !scrollRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const componentRect = componentRef.current.getBoundingClientRect();
|
|
||||||
const scrollRect = scrollRef.current.getBoundingClientRect();
|
|
||||||
const top = componentRect.top - scrollRect.top + scrollRef.current.scrollTop - topOffset;
|
|
||||||
|
|
||||||
// maintain current x scroll position
|
|
||||||
scrollRef.current.scrollTo(scrollRef.current.scrollLeft, top);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UseFollowComponentProps {
|
interface UseFollowComponentProps {
|
||||||
followRef: MutableRefObject<HTMLElement | null>;
|
followRef: MutableRefObject<HTMLElement | null>;
|
||||||
scrollRef: MutableRefObject<HTMLElement | null>;
|
scrollRef: MutableRefObject<HTMLElement | null>;
|
||||||
@@ -81,32 +62,3 @@ export default function useFollowComponent(props: UseFollowComponentProps) {
|
|||||||
|
|
||||||
return scrollToRefComponent;
|
return scrollToRefComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useFollowSelected(doFollow: boolean, topOffset = 100) {
|
|
||||||
const selectedEvenId = useSelectedEventId();
|
|
||||||
|
|
||||||
const selectedRef = useRef<HTMLTableRowElement>(null);
|
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!doFollow) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedEvenId && selectedRef.current && scrollRef.current) {
|
|
||||||
// Use requestAnimationFrame to ensure the component is fully loaded
|
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
snapToComponent(
|
|
||||||
{ current: selectedRef.current } as MutableRefObject<HTMLElement>,
|
|
||||||
{ current: scrollRef.current } as MutableRefObject<HTMLElement>,
|
|
||||||
topOffset,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [doFollow, selectedEvenId, topOffset]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
selectedRef,
|
|
||||||
scrollRef,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useOs, useViewportSize } from '@mantine/hooks';
|
import { useOs, useViewportSize } from '@mantine/hooks';
|
||||||
|
|
||||||
export function useIsMobileDevice(): boolean {
|
export function useIsMobile(): boolean {
|
||||||
const { width } = useViewportSize();
|
const { width } = useViewportSize();
|
||||||
const os = useOs();
|
const os = useOs();
|
||||||
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
import { useViewportSize } from '@mantine/hooks';
|
|
||||||
|
|
||||||
export function useIsMobileScreen(): boolean {
|
|
||||||
const { width } = useViewportSize();
|
|
||||||
|
|
||||||
return useMemo(() => width < 800, [width]);
|
|
||||||
}
|
|
||||||
@@ -4,12 +4,12 @@ import { useKeyDown } from '../../common/hooks/useKeyDown';
|
|||||||
|
|
||||||
import AboutPanel from './panel/about-panel/AboutPanel';
|
import AboutPanel from './panel/about-panel/AboutPanel';
|
||||||
import AutomationPanel from './panel/automations-panel/AutomationPanel';
|
import AutomationPanel from './panel/automations-panel/AutomationPanel';
|
||||||
import FeaturePanel from './panel/feature-panel/FeaturePanel';
|
import FeatureSettingsPanel from './panel/feature-settings-panel/FeatureSettingsPanel';
|
||||||
import ManagePanel from './panel/manage-panel/ManagePanel';
|
import GeneralPanel from './panel/general-panel/GeneralPanel';
|
||||||
import NetworkLogPanel from './panel/network-panel/NetworkLogPanel';
|
import NetworkLogPanel from './panel/network-panel/NetworkLogPanel';
|
||||||
import ProjectPanel from './panel/project-panel/ProjectPanel';
|
import ProjectPanel from './panel/project-panel/ProjectPanel';
|
||||||
import SettingsPanel from './panel/settings-panel/SettingsPanel';
|
|
||||||
import ShutdownPanel from './panel/shutdown-panel/ShutdownPanel';
|
import ShutdownPanel from './panel/shutdown-panel/ShutdownPanel';
|
||||||
|
import SourcesPanel from './panel/sources-panel/SourcesPanel';
|
||||||
import PanelContent from './panel-content/PanelContent';
|
import PanelContent from './panel-content/PanelContent';
|
||||||
import PanelList from './panel-list/PanelList';
|
import PanelList from './panel-list/PanelList';
|
||||||
import useAppSettingsNavigation from './useAppSettingsNavigation';
|
import useAppSettingsNavigation from './useAppSettingsNavigation';
|
||||||
@@ -25,11 +25,11 @@ export default function AppSettings() {
|
|||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<PanelList selectedPanel={panel} location={location} />
|
<PanelList selectedPanel={panel} location={location} />
|
||||||
<PanelContent onClose={close}>
|
<PanelContent onClose={close}>
|
||||||
{panel === 'settings' && <SettingsPanel location={location} />}
|
|
||||||
{panel === 'project' && <ProjectPanel location={location} setLocation={setLocation} />}
|
{panel === 'project' && <ProjectPanel location={location} setLocation={setLocation} />}
|
||||||
{panel === 'manage' && <ManagePanel location={location} />}
|
{panel === 'general' && <GeneralPanel location={location} />}
|
||||||
|
{panel === 'feature_settings' && <FeatureSettingsPanel location={location} />}
|
||||||
|
{panel === 'sources' && <SourcesPanel />}
|
||||||
{panel === 'automation' && <AutomationPanel location={location} />}
|
{panel === 'automation' && <AutomationPanel location={location} />}
|
||||||
{panel === 'sharing' && <FeaturePanel location={location} />}
|
|
||||||
{panel === 'network' && <NetworkLogPanel location={location} />}
|
{panel === 'network' && <NetworkLogPanel location={location} />}
|
||||||
{panel === 'about' && <AboutPanel />}
|
{panel === 'about' && <AboutPanel />}
|
||||||
{panel === 'shutdown' && <ShutdownPanel />}
|
{panel === 'shutdown' && <ShutdownPanel />}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
import { IoClose } from 'react-icons/io5';
|
import { IoClose } from 'react-icons/io5';
|
||||||
|
import { Button } from '@chakra-ui/react';
|
||||||
import Button from '../../../common/components/buttons/Button';
|
|
||||||
|
|
||||||
import style from './PanelContent.module.scss';
|
import style from './PanelContent.module.scss';
|
||||||
|
|
||||||
@@ -9,12 +8,14 @@ interface PanelContentProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PanelContent({ onClose, children }: PropsWithChildren<PanelContentProps>) {
|
export default function PanelContent(props: PropsWithChildren<PanelContentProps>) {
|
||||||
|
const { onClose, children } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.contentWrapper}>
|
<div className={style.contentWrapper}>
|
||||||
<div className={style.corner}>
|
<div className={style.corner}>
|
||||||
<Button size='large' onClick={onClose}>
|
<Button onClick={onClose} aria-label='close' rightIcon={<IoClose />} variant='ontime-subtle'>
|
||||||
Close settings <IoClose />
|
Close settings
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.content}>{children}</div>
|
<div className={style.content}>{children}</div>
|
||||||
|
|||||||
@@ -68,12 +68,12 @@ function PanelListItem(props: PanelListItemProps) {
|
|||||||
>
|
>
|
||||||
{panel.label}
|
{panel.label}
|
||||||
</li>
|
</li>
|
||||||
{panel.secondary?.map((secondary, index) => {
|
{panel.secondary?.map((secondary) => {
|
||||||
const id = secondary.id.split('__')[1];
|
const id = secondary.id.split('__')[1];
|
||||||
const secondaryClasses = cx([style.secondary, isSelected && location === id ? style.active : null]);
|
const secondaryClasses = cx([style.secondary, isSelected && location === id ? style.active : null]);
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={secondary.id + index}
|
key={secondary.id}
|
||||||
onClick={() => setLocation(secondary.id as SettingsOptionId)}
|
onClick={() => setLocation(secondary.id as SettingsOptionId)}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
isKeyEnter(event) && setLocation(secondary.id as SettingsOptionId);
|
isKeyEnter(event) && setLocation(secondary.id as SettingsOptionId);
|
||||||
|
|||||||
@@ -188,7 +188,6 @@ $inner-padding: 1rem;
|
|||||||
|
|
||||||
button {
|
button {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
margin-inline: auto;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
|
import { HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
|
||||||
import { IoAdd } from 'react-icons/io5';
|
import { IoAdd } from 'react-icons/io5';
|
||||||
|
import { Button } from '@chakra-ui/react';
|
||||||
|
|
||||||
import Button from '../../../common/components/buttons/Button';
|
|
||||||
import { cx } from '../../../common/utils/styleUtils';
|
import { cx } from '../../../common/utils/styleUtils';
|
||||||
|
|
||||||
import style from './PanelUtils.module.scss';
|
import style from './PanelUtils.module.scss';
|
||||||
@@ -68,8 +68,14 @@ export function TableEmpty({ label, handleClick }: { label?: string; handleClick
|
|||||||
<td colSpan={99}>
|
<td colSpan={99}>
|
||||||
<div>{label ?? 'No data yet'}</div>
|
<div>{label ?? 'No data yet'}</div>
|
||||||
{handleClick && (
|
{handleClick && (
|
||||||
<Button onClick={handleClick} disabled={!handleClick} variant='primary'>
|
<Button
|
||||||
New <IoAdd />
|
onClick={handleClick}
|
||||||
|
isDisabled={!handleClick}
|
||||||
|
variant='ontime-filled'
|
||||||
|
rightIcon={<IoAdd />}
|
||||||
|
size='sm'
|
||||||
|
>
|
||||||
|
New
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { Controller, useFieldArray, useForm } from 'react-hook-form';
|
import { Controller, useFieldArray, useForm } from 'react-hook-form';
|
||||||
import { IoAdd, IoTrash } from 'react-icons/io5';
|
import { IoAdd, IoTrash } from 'react-icons/io5';
|
||||||
import { Radio, RadioGroup, Select } from '@chakra-ui/react';
|
import { Button, IconButton, Input, Radio, RadioGroup, Select } from '@chakra-ui/react';
|
||||||
import {
|
import {
|
||||||
Automation,
|
Automation,
|
||||||
AutomationDTO,
|
AutomationDTO,
|
||||||
@@ -15,10 +15,7 @@ import {
|
|||||||
|
|
||||||
import { addAutomation, editAutomation, testOutput } from '../../../../common/api/automation';
|
import { addAutomation, editAutomation, testOutput } from '../../../../common/api/automation';
|
||||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import Button from '../../../../common/components/buttons/Button';
|
|
||||||
import IconButton from '../../../../common/components/buttons/IconButton';
|
|
||||||
import Info from '../../../../common/components/info/Info';
|
import Info from '../../../../common/components/info/Info';
|
||||||
import Input from '../../../../common/components/input/input/Input';
|
|
||||||
import ExternalLink from '../../../../common/components/link/external-link/ExternalLink';
|
import ExternalLink from '../../../../common/components/link/external-link/ExternalLink';
|
||||||
import Tag from '../../../../common/components/tag/Tag';
|
import Tag from '../../../../common/components/tag/Tag';
|
||||||
import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings';
|
import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings';
|
||||||
@@ -201,8 +198,10 @@ export default function AutomationForm(props: AutomationFormProps) {
|
|||||||
Title
|
Title
|
||||||
<Input
|
<Input
|
||||||
{...register('title', { required: { value: true, message: 'Required field' } })}
|
{...register('title', { required: { value: true, message: 'Required field' } })}
|
||||||
fluid
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
placeholder='Load preset'
|
placeholder='Load preset'
|
||||||
|
autoComplete='off'
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<Panel.Error>{errors.title?.message}</Panel.Error>
|
<Panel.Error>{errors.title?.message}</Panel.Error>
|
||||||
@@ -273,22 +272,43 @@ export default function AutomationForm(props: AutomationFormProps) {
|
|||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Value to match
|
Value to match
|
||||||
<Input {...register(`filters.${index}.value`)} fluid placeholder='<empty / no value>' />
|
<Input
|
||||||
|
{...register(`filters.${index}.value`)}
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
placeholder='<empty / no value>'
|
||||||
|
autoComplete='off'
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<div>
|
<div>
|
||||||
<span> </span>
|
<span> </span>
|
||||||
<div>
|
<div>
|
||||||
<IconButton aria-label='Delete' variant='ghosted-destructive' onClick={() => removeFilter(index)}>
|
<IconButton
|
||||||
<IoTrash />
|
aria-label='Delete'
|
||||||
</IconButton>
|
icon={<IoTrash />}
|
||||||
|
variant='ontime-ghosted'
|
||||||
|
size='sm'
|
||||||
|
color='#FA5656' // $red-500
|
||||||
|
onClick={() => removeFilter(index)}
|
||||||
|
isDisabled={false}
|
||||||
|
isLoading={false}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<div>
|
<div>
|
||||||
<Button type='submit' onClick={handleAddNewFilter}>
|
<Button
|
||||||
Add filter <IoAdd />
|
variant='ontime-subtle'
|
||||||
|
size='sm'
|
||||||
|
type='submit'
|
||||||
|
rightIcon={<IoAdd />}
|
||||||
|
onClick={handleAddNewFilter}
|
||||||
|
isDisabled={false}
|
||||||
|
isLoading={false}
|
||||||
|
>
|
||||||
|
Add filter
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -322,8 +342,10 @@ export default function AutomationForm(props: AutomationFormProps) {
|
|||||||
{...register(`outputs.${index}.targetIP`, {
|
{...register(`outputs.${index}.targetIP`, {
|
||||||
required: { value: true, message: 'Required field' },
|
required: { value: true, message: 'Required field' },
|
||||||
})}
|
})}
|
||||||
fluid
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
placeholder='127.0.0.1'
|
placeholder='127.0.0.1'
|
||||||
|
autoComplete='off'
|
||||||
/>
|
/>
|
||||||
<Panel.Error>{rowErrors?.targetIP?.message}</Panel.Error>
|
<Panel.Error>{rowErrors?.targetIP?.message}</Panel.Error>
|
||||||
</label>
|
</label>
|
||||||
@@ -336,32 +358,51 @@ export default function AutomationForm(props: AutomationFormProps) {
|
|||||||
max: { value: 65535, message: 'Port must be within range 1024 - 65535' },
|
max: { value: 65535, message: 'Port must be within range 1024 - 65535' },
|
||||||
min: { value: 1024, message: 'Port must be within range 1024 - 65535' },
|
min: { value: 1024, message: 'Port must be within range 1024 - 65535' },
|
||||||
})}
|
})}
|
||||||
fluid
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
type='number'
|
type='number'
|
||||||
maxLength={5}
|
maxLength={5}
|
||||||
placeholder='8000'
|
placeholder='8000'
|
||||||
|
autoComplete='off'
|
||||||
/>
|
/>
|
||||||
<Panel.Error>{rowErrors?.targetPort?.message}</Panel.Error>
|
<Panel.Error>{rowErrors?.targetPort?.message}</Panel.Error>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Address
|
Address
|
||||||
<Input {...register(`outputs.${index}.address`)} fluid placeholder='/cue/start' />
|
<Input
|
||||||
|
{...register(`outputs.${index}.address`)}
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
placeholder='/cue/start'
|
||||||
|
autoComplete='off'
|
||||||
|
/>
|
||||||
<Panel.Error>{rowErrors?.address?.message}</Panel.Error>
|
<Panel.Error>{rowErrors?.address?.message}</Panel.Error>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Arguments
|
Arguments
|
||||||
<TemplateInput {...register(`outputs.${index}.args`)} value={output.args} placeholder='1' />
|
<TemplateInput
|
||||||
|
{...register(`outputs.${index}.args`)}
|
||||||
|
value={output.args}
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
placeholder='1'
|
||||||
|
/>
|
||||||
<Panel.Error>{rowErrors?.args?.message}</Panel.Error>
|
<Panel.Error>{rowErrors?.args?.message}</Panel.Error>
|
||||||
</label>
|
</label>
|
||||||
<div>
|
<div>
|
||||||
<span> </span>
|
<span> </span>
|
||||||
<Panel.InlineElements relation='inner'>
|
<Panel.InlineElements relation='inner'>
|
||||||
<Button variant='ghosted-white' onClick={() => handleTestOSCOutput(index)}>
|
<Button size='sm' variant='ontime-ghosted-white' onClick={() => handleTestOSCOutput(index)}>
|
||||||
Test
|
Test
|
||||||
</Button>
|
</Button>
|
||||||
<IconButton aria-label='Delete' variant='ghosted-destructive' onClick={() => removeOutput(index)}>
|
<IconButton
|
||||||
<IoTrash />
|
aria-label='Delete'
|
||||||
</IconButton>
|
icon={<IoTrash />}
|
||||||
|
variant='ontime-ghosted'
|
||||||
|
size='sm'
|
||||||
|
onClick={() => removeOutput(index)}
|
||||||
|
color='#FA5656' // $red-500
|
||||||
|
/>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -388,20 +429,27 @@ export default function AutomationForm(props: AutomationFormProps) {
|
|||||||
message: 'HTTP messages should target http:// or https://',
|
message: 'HTTP messages should target http:// or https://',
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
fluid
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
placeholder='http://127.0.0.1/start/1'
|
placeholder='http://127.0.0.1/start/1'
|
||||||
|
autoComplete='off'
|
||||||
/>
|
/>
|
||||||
<Panel.Error>{rowErrors?.url?.message}</Panel.Error>
|
<Panel.Error>{rowErrors?.url?.message}</Panel.Error>
|
||||||
</label>
|
</label>
|
||||||
<div>
|
<div>
|
||||||
<span> </span>
|
<span> </span>
|
||||||
<Panel.InlineElements relation='inner'>
|
<Panel.InlineElements relation='inner'>
|
||||||
<Button variant='ghosted-white' onClick={() => handleTestHTTPOutput(index)}>
|
<Button size='sm' variant='ontime-ghosted-white' onClick={() => handleTestHTTPOutput(index)}>
|
||||||
Test
|
Test
|
||||||
</Button>
|
</Button>
|
||||||
<IconButton aria-label='Delete' variant='ghosted-destructive' onClick={() => removeOutput(index)}>
|
<IconButton
|
||||||
<IoTrash />
|
aria-label='Delete'
|
||||||
</IconButton>
|
icon={<IoTrash />}
|
||||||
|
variant='ontime-ghosted'
|
||||||
|
size='sm'
|
||||||
|
onClick={() => removeOutput(index)}
|
||||||
|
color='#FA5656' // $red-500
|
||||||
|
/>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -431,12 +479,17 @@ export default function AutomationForm(props: AutomationFormProps) {
|
|||||||
>
|
>
|
||||||
<span> </span>
|
<span> </span>
|
||||||
<Panel.InlineElements relation='inner'>
|
<Panel.InlineElements relation='inner'>
|
||||||
<Button variant='ghosted-white' onClick={() => handleTestOntimeAction(index)}>
|
<Button size='sm' variant='ontime-ghosted-white' onClick={() => handleTestOntimeAction(index)}>
|
||||||
Test
|
Test
|
||||||
</Button>
|
</Button>
|
||||||
<IconButton aria-label='Delete' variant='ghosted-destructive' onClick={() => removeOutput(index)}>
|
<IconButton
|
||||||
<IoTrash />
|
aria-label='Delete'
|
||||||
</IconButton>
|
icon={<IoTrash />}
|
||||||
|
variant='ontime-ghosted'
|
||||||
|
size='sm'
|
||||||
|
onClick={() => removeOutput(index)}
|
||||||
|
color='#FA5656' // $red-500
|
||||||
|
/>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
</OntimeActionForm>
|
</OntimeActionForm>
|
||||||
</div>
|
</div>
|
||||||
@@ -447,22 +500,24 @@ export default function AutomationForm(props: AutomationFormProps) {
|
|||||||
return null;
|
return null;
|
||||||
})}
|
})}
|
||||||
<Panel.InlineElements relation='inner'>
|
<Panel.InlineElements relation='inner'>
|
||||||
<Button onClick={handleAddNewOSCOutput}>
|
<Button variant='ontime-subtle' rightIcon={<IoAdd />} size='sm' onClick={handleAddNewOSCOutput}>
|
||||||
OSC <IoAdd />
|
OSC
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleAddNewHTTPOutput}>
|
<Button variant='ontime-subtle' rightIcon={<IoAdd />} size='sm' onClick={handleAddNewHTTPOutput}>
|
||||||
HTTP <IoAdd />
|
HTTP
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleAddnewOntimeAction}>
|
<Button variant='ontime-subtle' rightIcon={<IoAdd />} size='sm' onClick={handleAddnewOntimeAction}>
|
||||||
Ontime action <IoAdd />
|
Ontime action
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Panel.InlineElements align='end'>
|
<Panel.InlineElements align='end'>
|
||||||
{errors?.root && <Panel.Error>{errors.root.message}</Panel.Error>}
|
{errors?.root && <Panel.Error>{errors.root.message}</Panel.Error>}
|
||||||
<Button onClick={onClose}>Cancel</Button>
|
<Button variant='ontime-subtle' size='sm' onClick={onClose}>
|
||||||
<Button variant='primary' type='submit' disabled={!canSubmit} loading={isSubmitting}>
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant='ontime-filled' size='sm' type='submit' isDisabled={!canSubmit} isLoading={isSubmitting}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
|
|||||||
+11
-9
@@ -1,11 +1,9 @@
|
|||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { Switch } from '@chakra-ui/react';
|
import { Button, Input, Switch } from '@chakra-ui/react';
|
||||||
|
|
||||||
import { editAutomationSettings } from '../../../../common/api/automation';
|
import { editAutomationSettings } from '../../../../common/api/automation';
|
||||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import Button from '../../../../common/components/buttons/Button';
|
|
||||||
import Info from '../../../../common/components/info/Info';
|
import Info from '../../../../common/components/info/Info';
|
||||||
import Input from '../../../../common/components/input/input/Input';
|
|
||||||
import ExternalLink from '../../../../common/components/link/external-link/ExternalLink';
|
import ExternalLink from '../../../../common/components/link/external-link/ExternalLink';
|
||||||
import { preventEscape } from '../../../../common/utils/keyEvent';
|
import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||||
import { isOnlyNumbers } from '../../../../common/utils/regex';
|
import { isOnlyNumbers } from '../../../../common/utils/regex';
|
||||||
@@ -59,15 +57,16 @@ export default function AutomationSettingsForm(props: AutomationSettingsProps) {
|
|||||||
<Panel.SubHeader>
|
<Panel.SubHeader>
|
||||||
Automation settings
|
Automation settings
|
||||||
<Panel.InlineElements>
|
<Panel.InlineElements>
|
||||||
<Button variant='ghosted' onClick={onReset} disabled={!canSubmit}>
|
<Button variant='ontime-ghosted' size='sm' onClick={onReset} isDisabled={!canSubmit}>
|
||||||
Revert to saved
|
Revert to saved
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant='primary'
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
type='submit'
|
type='submit'
|
||||||
form='automation-settings-form'
|
form='automation-settings-form'
|
||||||
disabled={!canSubmit}
|
isDisabled={!canSubmit}
|
||||||
loading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
@@ -140,10 +139,13 @@ export default function AutomationSettingsForm(props: AutomationSettingsProps) {
|
|||||||
<Input
|
<Input
|
||||||
id='oscPortIn'
|
id='oscPortIn'
|
||||||
placeholder='8888'
|
placeholder='8888'
|
||||||
|
width='5rem'
|
||||||
maxLength={5}
|
maxLength={5}
|
||||||
style={{ textAlign: 'right', width: '5rem' }}
|
size='sm'
|
||||||
|
textAlign='right'
|
||||||
|
variant='ontime-filled'
|
||||||
type='number'
|
type='number'
|
||||||
fluid
|
autoComplete='off'
|
||||||
{...register('oscPortIn', {
|
{...register('oscPortIn', {
|
||||||
required: { value: true, message: 'Required field' },
|
required: { value: true, message: 'Required field' },
|
||||||
max: { value: 65535, message: 'Port must be within range 1024 - 65535' },
|
max: { value: 65535, message: 'Port must be within range 1024 - 65535' },
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { Fragment, useState } from 'react';
|
import { Fragment, useState } from 'react';
|
||||||
import { IoAdd, IoPencil, IoTrash } from 'react-icons/io5';
|
import { IoAdd, IoPencil, IoTrash } from 'react-icons/io5';
|
||||||
|
import { Button, IconButton } from '@chakra-ui/react';
|
||||||
import { AutomationDTO, NormalisedAutomation } from 'ontime-types';
|
import { AutomationDTO, NormalisedAutomation } from 'ontime-types';
|
||||||
|
|
||||||
import { deleteAutomation } from '../../../../common/api/automation';
|
import { deleteAutomation } from '../../../../common/api/automation';
|
||||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import Button from '../../../../common/components/buttons/Button';
|
|
||||||
import IconButton from '../../../../common/components/buttons/IconButton';
|
|
||||||
import Tag from '../../../../common/components/tag/Tag';
|
import Tag from '../../../../common/components/tag/Tag';
|
||||||
import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings';
|
import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings';
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
@@ -47,11 +46,14 @@ export default function AutomationsList(props: AutomationsListProps) {
|
|||||||
<Panel.SubHeader>
|
<Panel.SubHeader>
|
||||||
Manage automations
|
Manage automations
|
||||||
<Button
|
<Button
|
||||||
|
variant='ontime-subtle'
|
||||||
|
rightIcon={<IoAdd />}
|
||||||
|
size='sm'
|
||||||
type='submit'
|
type='submit'
|
||||||
disabled={Boolean(automationFormData)}
|
isDisabled={Boolean(automationFormData)}
|
||||||
onClick={() => setAutomationFormData(automationPlaceholder)}
|
onClick={() => setAutomationFormData(automationPlaceholder)}
|
||||||
>
|
>
|
||||||
New <IoAdd />
|
New
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.SubHeader>
|
</Panel.SubHeader>
|
||||||
|
|
||||||
@@ -92,19 +94,21 @@ export default function AutomationsList(props: AutomationsListProps) {
|
|||||||
<td>{automations[automationId].outputs.length}</td>
|
<td>{automations[automationId].outputs.length}</td>
|
||||||
<Panel.InlineElements align='end' relation='inner' as='td'>
|
<Panel.InlineElements align='end' relation='inner' as='td'>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant='ghosted-white'
|
size='sm'
|
||||||
|
variant='ontime-ghosted'
|
||||||
|
color='#e2e2e2' // $gray-200
|
||||||
|
icon={<IoPencil />}
|
||||||
aria-label='Edit entry'
|
aria-label='Edit entry'
|
||||||
onClick={() => setAutomationFormData(automations[automationId])}
|
onClick={() => setAutomationFormData(automations[automationId])}
|
||||||
>
|
/>
|
||||||
<IoPencil />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
variant='ghosted-destructive'
|
size='sm'
|
||||||
|
variant='ontime-ghosted'
|
||||||
|
color='#FA5656' // $red-500
|
||||||
|
icon={<IoTrash />}
|
||||||
aria-label='Delete entry'
|
aria-label='Delete entry'
|
||||||
onClick={() => handleDelete(automationId)}
|
onClick={() => handleDelete(automationId)}
|
||||||
>
|
/>
|
||||||
<IoTrash />
|
|
||||||
</IconButton>
|
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
</tr>
|
</tr>
|
||||||
{deleteError && (
|
{deleteError && (
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { PropsWithChildren, useState } from 'react';
|
import { PropsWithChildren, useState } from 'react';
|
||||||
import { UseFormRegister, UseFormSetValue } from 'react-hook-form';
|
import { UseFormRegister, UseFormSetValue } from 'react-hook-form';
|
||||||
import { Select } from '@chakra-ui/react';
|
import { Input, Select } from '@chakra-ui/react';
|
||||||
import { AutomationDTO, OntimeAction } from 'ontime-types';
|
import { AutomationDTO, OntimeAction } from 'ontime-types';
|
||||||
|
|
||||||
import Input from '../../../../common/components/input/input/Input';
|
|
||||||
import { cx } from '../../../../common/utils/styleUtils';
|
import { cx } from '../../../../common/utils/styleUtils';
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
@@ -68,8 +67,10 @@ export default function OntimeActionForm(props: PropsWithChildren<OntimeActionFo
|
|||||||
{...register(`outputs.${index}.time`, {
|
{...register(`outputs.${index}.time`, {
|
||||||
required: { value: true, message: 'Required field' },
|
required: { value: true, message: 'Required field' },
|
||||||
})}
|
})}
|
||||||
fluid
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
placeholder='eg: 10m5s'
|
placeholder='eg: 10m5s'
|
||||||
|
autoComplete='off'
|
||||||
/>
|
/>
|
||||||
<Panel.Error>{rowErrors?.time?.message}</Panel.Error>
|
<Panel.Error>{rowErrors?.time?.message}</Panel.Error>
|
||||||
</label>
|
</label>
|
||||||
@@ -79,7 +80,13 @@ export default function OntimeActionForm(props: PropsWithChildren<OntimeActionFo
|
|||||||
<>
|
<>
|
||||||
<label>
|
<label>
|
||||||
Text (leave empty for no change)
|
Text (leave empty for no change)
|
||||||
<Input {...register(`outputs.${index}.text`)} fluid placeholder='eg: Timer is finished' />
|
<Input
|
||||||
|
{...register(`outputs.${index}.text`)}
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
placeholder='eg: Timer is finished'
|
||||||
|
autoComplete='off'
|
||||||
|
/>
|
||||||
<Panel.Error>{rowErrors?.text?.message}</Panel.Error>
|
<Panel.Error>{rowErrors?.text?.message}</Panel.Error>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { Select } from '@chakra-ui/react';
|
import { Button, Input, Select } from '@chakra-ui/react';
|
||||||
import { NormalisedAutomation, TimerLifeCycle, TriggerDTO } from 'ontime-types';
|
import { NormalisedAutomation, TimerLifeCycle, TriggerDTO } from 'ontime-types';
|
||||||
|
|
||||||
import { addTrigger, editTrigger } from '../../../../common/api/automation';
|
import { addTrigger, editTrigger } from '../../../../common/api/automation';
|
||||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import Button from '../../../../common/components/buttons/Button';
|
|
||||||
import Input from '../../../../common/components/input/input/Input';
|
|
||||||
import { preventEscape } from '../../../../common/utils/keyEvent';
|
import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
@@ -89,7 +87,9 @@ export default function TriggerForm(props: TriggerFormProps) {
|
|||||||
Title
|
Title
|
||||||
<Input
|
<Input
|
||||||
{...register('title', { required: { value: true, message: 'Required field' } })}
|
{...register('title', { required: { value: true, message: 'Required field' } })}
|
||||||
fluid
|
size='sm'
|
||||||
|
variant='ontime-filled'
|
||||||
|
autoComplete='off'
|
||||||
defaultValue={initialTitle}
|
defaultValue={initialTitle}
|
||||||
/>
|
/>
|
||||||
<Panel.Error>{errors.title?.message}</Panel.Error>
|
<Panel.Error>{errors.title?.message}</Panel.Error>
|
||||||
@@ -127,10 +127,10 @@ export default function TriggerForm(props: TriggerFormProps) {
|
|||||||
<Panel.Error>{errors.automationId?.message}</Panel.Error>
|
<Panel.Error>{errors.automationId?.message}</Panel.Error>
|
||||||
</label>
|
</label>
|
||||||
<Panel.InlineElements align='end'>
|
<Panel.InlineElements align='end'>
|
||||||
<Button disabled={isSubmitting} onClick={onCancel}>
|
<Button size='sm' variant='ontime-subtle' isDisabled={isSubmitting} onClick={onCancel}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type='submit' variant='primary' disabled={!canSubmit} loading={isSubmitting}>
|
<Button type='submit' size='sm' variant='ontime-filled' isDisabled={!canSubmit} isLoading={isSubmitting}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Fragment, useMemo, useState } from 'react';
|
import { Fragment, useMemo, useState } from 'react';
|
||||||
import { IoAdd } from 'react-icons/io5';
|
import { IoAdd } from 'react-icons/io5';
|
||||||
|
import { Button } from '@chakra-ui/react';
|
||||||
import { NormalisedAutomation, Trigger } from 'ontime-types';
|
import { NormalisedAutomation, Trigger } from 'ontime-types';
|
||||||
|
|
||||||
import { deleteTrigger } from '../../../../common/api/automation';
|
import { deleteTrigger } from '../../../../common/api/automation';
|
||||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import Button from '../../../../common/components/buttons/Button';
|
|
||||||
import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings';
|
import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings';
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
@@ -47,8 +47,17 @@ export default function TriggersList(props: TriggersListProps) {
|
|||||||
<Panel.Card>
|
<Panel.Card>
|
||||||
<Panel.SubHeader>
|
<Panel.SubHeader>
|
||||||
Manage triggers
|
Manage triggers
|
||||||
<Button type='submit' form='trigger-form' disabled={!canAdd} loading={false} onClick={() => setShowForm(true)}>
|
<Button
|
||||||
New <IoAdd />
|
variant='ontime-subtle'
|
||||||
|
rightIcon={<IoAdd />}
|
||||||
|
size='sm'
|
||||||
|
type='submit'
|
||||||
|
form='trigger-form'
|
||||||
|
isDisabled={!canAdd}
|
||||||
|
isLoading={false}
|
||||||
|
onClick={() => setShowForm(true)}
|
||||||
|
>
|
||||||
|
New
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.SubHeader>
|
</Panel.SubHeader>
|
||||||
<Panel.Divider />
|
<Panel.Divider />
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { IoPencil, IoTrash, IoWarningOutline } from 'react-icons/io5';
|
import { IoPencil, IoTrash, IoWarningOutline } from 'react-icons/io5';
|
||||||
|
import { IconButton } from '@chakra-ui/react';
|
||||||
import { NormalisedAutomation, TimerLifeCycle } from 'ontime-types';
|
import { NormalisedAutomation, TimerLifeCycle } from 'ontime-types';
|
||||||
|
|
||||||
import IconButton from '../../../../common/components/buttons/IconButton';
|
|
||||||
import Tag from '../../../../common/components/tag/Tag';
|
import Tag from '../../../../common/components/tag/Tag';
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
@@ -62,12 +62,22 @@ export default function TriggersListItem(props: TriggersListItemProps) {
|
|||||||
<Tag>{automations?.[automationId]?.title}</Tag>
|
<Tag>{automations?.[automationId]?.title}</Tag>
|
||||||
</td>
|
</td>
|
||||||
<Panel.InlineElements align='end' relation='inner' as='td'>
|
<Panel.InlineElements align='end' relation='inner' as='td'>
|
||||||
<IconButton variant='ghosted-white' aria-label='Edit entry' onClick={() => setIsEditing(true)}>
|
<IconButton
|
||||||
<IoPencil />
|
size='sm'
|
||||||
</IconButton>
|
variant='ontime-ghosted'
|
||||||
<IconButton variant='ghosted-destructive' aria-label='Delete entry' onClick={handleDelete}>
|
color='#e2e2e2' // $gray-200
|
||||||
<IoTrash />
|
icon={<IoPencil />}
|
||||||
</IconButton>
|
aria-label='Edit entry'
|
||||||
|
onClick={() => setIsEditing(true)}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
size='sm'
|
||||||
|
variant='ontime-ghosted'
|
||||||
|
color='#FA5656' // $red-500
|
||||||
|
icon={<IoTrash />}
|
||||||
|
aria-label='Delete entry'
|
||||||
|
onClick={handleDelete}
|
||||||
|
/>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import { forwardRef, useMemo, useState } from 'react';
|
import { forwardRef, useMemo, useState } from 'react';
|
||||||
|
import { type InputProps, Input } from '@chakra-ui/react';
|
||||||
import { mergeRefs, useClickOutside } from '@mantine/hooks';
|
import { mergeRefs, useClickOutside } from '@mantine/hooks';
|
||||||
|
|
||||||
import Input, { type InputProps } from '../../../../../common/components/input/input/Input';
|
|
||||||
import useCustomFields from '../../../../../common/hooks-query/useCustomFields';
|
import useCustomFields from '../../../../../common/hooks-query/useCustomFields';
|
||||||
|
|
||||||
import { makeAutoCompleteList, matchRemaining, selectFromLastTemplate } from './templateInput.utils';
|
import { makeAutoCompleteList, matchRemaining, selectFromLastTemplate } from './templateInput.utils';
|
||||||
@@ -53,7 +53,7 @@ const TemplateInput = forwardRef(function TemplateInput(props: TemplateInputProp
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.wrapper} ref={mergeRefs(localRef, ref)}>
|
<div className={style.wrapper} ref={mergeRefs(localRef, ref)}>
|
||||||
<Input value={inputValue} {...rest} onChange={handleInputChange} fluid />
|
<Input value={inputValue} {...rest} onChange={handleInputChange} autoComplete='off' autoCorrect='off' />
|
||||||
{showSuggestions && suggestions.length > 0 && (
|
{showSuggestions && suggestions.length > 0 && (
|
||||||
<ul className={style.suggestions}>
|
<ul className={style.suggestions}>
|
||||||
{suggestions.map((suggestion) => (
|
{suggestions.map((suggestion) => (
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import * as Panel from '../../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import ClientList from './ClientList';
|
import ClientList from './ClientList';
|
||||||
|
|
||||||
+25
-26
@@ -1,34 +1,32 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { Badge, Button, useDisclosure } from '@chakra-ui/react';
|
||||||
import { Client } from 'ontime-types';
|
import { Client } from 'ontime-types';
|
||||||
|
|
||||||
import Button from '../../../../../common/components/buttons/Button';
|
import { RedirectClientModal } from '../../../../common/components/client-modal/RedirectClientModal';
|
||||||
import { RedirectClientModal } from '../../../../../common/components/client-modal/RedirectClientModal';
|
import { RenameClientModal } from '../../../../common/components/client-modal/RenameClientModal';
|
||||||
import { RenameClientModal } from '../../../../../common/components/client-modal/RenameClientModal';
|
import { setClientRemote } from '../../../../common/hooks/useSocket';
|
||||||
import Tag from '../../../../../common/components/tag/Tag';
|
import { useClientStore } from '../../../../common/stores/clientStore';
|
||||||
import { setClientRemote } from '../../../../../common/hooks/useSocket';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
import { useClientStore } from '../../../../../common/stores/clientStore';
|
|
||||||
import * as Panel from '../../../panel-utils/PanelUtils';
|
|
||||||
|
|
||||||
import style from './ClientControlPanel.module.scss';
|
import style from './ClientControlPanel.module.scss';
|
||||||
|
|
||||||
export default function ClientList() {
|
export default function ClientList() {
|
||||||
const id = useClientStore((store) => store.id);
|
const id = useClientStore((store) => store.id);
|
||||||
const clients = useClientStore((store) => store.clients);
|
const clients = useClientStore((store) => store.clients);
|
||||||
const [isOpenRedirect, redirectHandler] = useDisclosure();
|
const { isOpen: isOpenRedirect, onOpen: onOpenRedirect, onClose: onCloseRedirect } = useDisclosure();
|
||||||
const [isOpenRename, renameHandler] = useDisclosure();
|
const { isOpen: isOpenRename, onOpen: onOpenRename, onClose: onCloseRename } = useDisclosure();
|
||||||
const { setIdentify } = setClientRemote;
|
const { setIdentify } = setClientRemote;
|
||||||
|
|
||||||
const [targetId, setTargetId] = useState('');
|
const [targetId, setTargetId] = useState('');
|
||||||
|
|
||||||
const openRename = (targetId: string) => {
|
const openRename = (targetId: string) => {
|
||||||
setTargetId(targetId);
|
setTargetId(targetId);
|
||||||
renameHandler.open();
|
onOpenRename();
|
||||||
};
|
};
|
||||||
|
|
||||||
const openRedirect = (targetId: string) => {
|
const openRedirect = (targetId: string) => {
|
||||||
setTargetId(targetId);
|
setTargetId(targetId);
|
||||||
redirectHandler.open();
|
onOpenRedirect();
|
||||||
};
|
};
|
||||||
|
|
||||||
const ontimeClients = Object.entries(clients).filter(([_, { type }]) => type === 'ontime');
|
const ontimeClients = Object.entries(clients).filter(([_, { type }]) => type === 'ontime');
|
||||||
@@ -45,16 +43,11 @@ export default function ClientList() {
|
|||||||
origin={targetClient.origin}
|
origin={targetClient.origin}
|
||||||
currentPath={targetClient.path}
|
currentPath={targetClient.path}
|
||||||
isOpen={isOpenRedirect}
|
isOpen={isOpenRedirect}
|
||||||
onClose={redirectHandler.close}
|
onClose={onCloseRedirect}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isOpenRename && (
|
{isOpenRename && (
|
||||||
<RenameClientModal
|
<RenameClientModal id={targetId} name={targetClient?.name} isOpen={isOpenRename} onClose={onCloseRename} />
|
||||||
id={targetId}
|
|
||||||
name={targetClient?.name}
|
|
||||||
isOpen={isOpenRename}
|
|
||||||
onClose={renameHandler.close}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<Panel.Section>
|
<Panel.Section>
|
||||||
<Panel.Title>Ontime Clients ({ontimeClients.length})</Panel.Title>
|
<Panel.Title>Ontime Clients ({ontimeClients.length})</Panel.Title>
|
||||||
@@ -73,16 +66,20 @@ export default function ClientList() {
|
|||||||
return (
|
return (
|
||||||
<tr key={key}>
|
<tr key={key}>
|
||||||
<Panel.InlineElements relation='inner' as='td'>
|
<Panel.InlineElements relation='inner' as='td'>
|
||||||
{isCurrent && <Tag>SELF</Tag>}
|
{isCurrent && (
|
||||||
|
<Badge variant='outline' colorScheme='yellow' size='xs'>
|
||||||
|
self
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
{name}
|
{name}
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
<td>{path}</td>
|
<td>{path}</td>
|
||||||
<Panel.InlineElements relation='inner'>
|
<Panel.InlineElements relation='inner'>
|
||||||
<Button
|
<Button
|
||||||
size='small'
|
size='xs'
|
||||||
className={`${identify ? style.blink : ''}`}
|
className={`${identify ? style.blink : ''}`}
|
||||||
disabled={isCurrent}
|
isDisabled={isCurrent}
|
||||||
variant={identify ? 'primary' : 'subtle'}
|
variant={identify ? 'ontime-filled' : 'ontime-subtle'}
|
||||||
data-testid={isCurrent ? '' : 'not-self-identify'}
|
data-testid={isCurrent ? '' : 'not-self-identify'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIdentify({ target: key, identify: !identify });
|
setIdentify({ target: key, identify: !identify });
|
||||||
@@ -91,7 +88,8 @@ export default function ClientList() {
|
|||||||
Identify
|
Identify
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size='small'
|
size='xs'
|
||||||
|
variant='ontime-subtle'
|
||||||
data-testid={isCurrent ? '' : 'not-self-rename'}
|
data-testid={isCurrent ? '' : 'not-self-rename'}
|
||||||
onClick={() => openRename(key)}
|
onClick={() => openRename(key)}
|
||||||
>
|
>
|
||||||
@@ -99,8 +97,9 @@ export default function ClientList() {
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size='small'
|
size='xs'
|
||||||
disabled={isCurrent}
|
variant='ontime-subtle'
|
||||||
|
isDisabled={isCurrent}
|
||||||
data-testid={isCurrent ? '' : 'not-self-redirect'}
|
data-testid={isCurrent ? '' : 'not-self-redirect'}
|
||||||
onClick={() => openRedirect(key)}
|
onClick={() => openRedirect(key)}
|
||||||
>
|
>
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
.fit {
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aliasConstrain {
|
|
||||||
min-width: 12em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullWidth {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
|
||||||
import { isOntimeCloud } from '../../../../externals';
|
|
||||||
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
|
||||||
import InfoNif from '../network-panel/NetworkInterfaces';
|
|
||||||
|
|
||||||
import GenerateLinkFormExport from './GenerateLinkFormExport';
|
|
||||||
import ReportSettings from './ReportSettings';
|
|
||||||
import UrlPresetsForm from './UrlPresetsForm';
|
|
||||||
|
|
||||||
export default function FeaturePanel({ location }: PanelBaseProps) {
|
|
||||||
const presetsRef = useScrollIntoView<HTMLDivElement>('presets', location);
|
|
||||||
const linkRef = useScrollIntoView<HTMLDivElement>('link', location);
|
|
||||||
const reportRef = useScrollIntoView<HTMLDivElement>('report', location);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Panel.Header>Sharing and reporting</Panel.Header>
|
|
||||||
<div ref={presetsRef}>
|
|
||||||
<UrlPresetsForm />
|
|
||||||
</div>
|
|
||||||
<div ref={linkRef}>
|
|
||||||
<Panel.Section>
|
|
||||||
<Panel.Card>
|
|
||||||
<Panel.SubHeader>Share Ontime Link</Panel.SubHeader>
|
|
||||||
<Panel.Divider />
|
|
||||||
{!isOntimeCloud && (
|
|
||||||
<>
|
|
||||||
<Panel.Paragraph>Ontime is streaming on the following network interfaces</Panel.Paragraph>
|
|
||||||
<InfoNif />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<GenerateLinkFormExport />
|
|
||||||
</Panel.Card>
|
|
||||||
</Panel.Section>
|
|
||||||
</div>
|
|
||||||
<div ref={reportRef}>
|
|
||||||
<ReportSettings />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
import useInfo from '../../../../common/hooks-query/useInfo';
|
|
||||||
import useUrlPresets from '../../../../common/hooks-query/useUrlPresets';
|
|
||||||
|
|
||||||
import GenerateLinkForm from './GenerateLinkForm';
|
|
||||||
|
|
||||||
interface GenerateLinkFormExportProps {
|
|
||||||
lockedPath?: { value: string; label: string };
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function GenerateLinkFormExport({ lockedPath }: GenerateLinkFormExportProps) {
|
|
||||||
const { data: infoData } = useInfo();
|
|
||||||
const { data: urlPresetData } = useUrlPresets({ skip: lockedPath === undefined });
|
|
||||||
|
|
||||||
const hostOptions = useMemo(
|
|
||||||
() =>
|
|
||||||
infoData.networkInterfaces.map((nif) => ({
|
|
||||||
value: nif.address,
|
|
||||||
label: `${nif.name} - ${nif.address}`,
|
|
||||||
})),
|
|
||||||
[infoData.networkInterfaces],
|
|
||||||
);
|
|
||||||
|
|
||||||
const pathOptions = useMemo(() => {
|
|
||||||
if (lockedPath) {
|
|
||||||
return [{ value: lockedPath.value, label: lockedPath.label }];
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
{ value: 'timer', label: 'Timer' },
|
|
||||||
{ value: 'cuesheet', label: 'Cuesheet' },
|
|
||||||
{ value: 'op', label: 'Operator' },
|
|
||||||
{ value: '', label: 'Companion' },
|
|
||||||
...urlPresetData.map((preset) => ({
|
|
||||||
value: preset.alias,
|
|
||||||
label: `Preset: ${preset.alias}`,
|
|
||||||
})),
|
|
||||||
];
|
|
||||||
}, [lockedPath, urlPresetData]);
|
|
||||||
|
|
||||||
return <GenerateLinkForm hostOptions={hostOptions} pathOptions={pathOptions} isLockedToView={Boolean(lockedPath)} />;
|
|
||||||
}
|
|
||||||
+8
-4
@@ -14,12 +14,16 @@
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fit {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aliasConstrain {
|
||||||
|
min-width: 12em;
|
||||||
|
}
|
||||||
|
|
||||||
.twoCols {
|
.twoCols {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.current {
|
|
||||||
background-color: $blue-1100;
|
|
||||||
}
|
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||||
|
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
||||||
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
|
import CustomFields from './custom-fields/CustomFields';
|
||||||
|
import ReportSettings from './ReportSettings';
|
||||||
|
import UrlPresetsForm from './UrlPresetsForm';
|
||||||
|
|
||||||
|
export default function FeatureSettingsPanel({ location }: PanelBaseProps) {
|
||||||
|
const customFieldsRef = useScrollIntoView<HTMLDivElement>('custom', location);
|
||||||
|
const urlPresetsRef = useScrollIntoView<HTMLDivElement>('urlpresets', location);
|
||||||
|
const reportRef = useScrollIntoView<HTMLDivElement>('report', location);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Panel.Header>Feature Settings</Panel.Header>
|
||||||
|
<div ref={customFieldsRef}>
|
||||||
|
<CustomFields />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref={urlPresetsRef}>
|
||||||
|
<UrlPresetsForm />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref={reportRef}>
|
||||||
|
<ReportSettings />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
+16
-5
@@ -1,9 +1,9 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { IoTrashBin } from 'react-icons/io5';
|
import { IoTrashBin } from 'react-icons/io5';
|
||||||
|
import { Button } from '@chakra-ui/react';
|
||||||
|
|
||||||
import { deleteAllReport } from '../../../../common/api/report';
|
import { deleteAllReport } from '../../../../common/api/report';
|
||||||
import { createBlob, downloadBlob } from '../../../../common/api/utils';
|
import { createBlob, downloadBlob } from '../../../../common/api/utils';
|
||||||
import Button from '../../../../common/components/buttons/Button';
|
|
||||||
import useReport from '../../../../common/hooks-query/useReport';
|
import useReport from '../../../../common/hooks-query/useReport';
|
||||||
import useRundown from '../../../../common/hooks-query/useRundown';
|
import useRundown from '../../../../common/hooks-query/useRundown';
|
||||||
import { cx } from '../../../../common/utils/styleUtils';
|
import { cx } from '../../../../common/utils/styleUtils';
|
||||||
@@ -41,12 +41,23 @@ export default function ReportSettings() {
|
|||||||
<Panel.Title>
|
<Panel.Title>
|
||||||
Manage report
|
Manage report
|
||||||
<Panel.InlineElements>
|
<Panel.InlineElements>
|
||||||
<Button onClick={() => downloadCSV(combinedReport)} disabled={combinedReport.length === 0}>
|
<Button
|
||||||
<IoTrashBin />
|
variant='ontime-subtle'
|
||||||
|
leftIcon={<IoTrashBin />}
|
||||||
|
size='sm'
|
||||||
|
onClick={() => downloadCSV(combinedReport)}
|
||||||
|
isDisabled={combinedReport.length === 0}
|
||||||
|
>
|
||||||
Export CSV
|
Export CSV
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant='subtle-destructive' onClick={clearReport} disabled={combinedReport.length === 0}>
|
<Button
|
||||||
<IoTrashBin />
|
variant='ontime-subtle'
|
||||||
|
leftIcon={<IoTrashBin />}
|
||||||
|
size='sm'
|
||||||
|
color='#FA5656'
|
||||||
|
onClick={clearReport}
|
||||||
|
isDisabled={combinedReport.length === 0}
|
||||||
|
>
|
||||||
Clear All
|
Clear All
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
+17
-15
@@ -1,16 +1,13 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useFieldArray, useForm } from 'react-hook-form';
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
import { IoAdd, IoOpenOutline, IoTrash } from 'react-icons/io5';
|
import { IoAdd, IoOpenOutline, IoTrash } from 'react-icons/io5';
|
||||||
import { Switch } from '@chakra-ui/react';
|
import { Button, IconButton, Input, Switch } from '@chakra-ui/react';
|
||||||
import { URLPreset } from 'ontime-types';
|
import { URLPreset } from 'ontime-types';
|
||||||
|
|
||||||
import { postUrlPresets } from '../../../../common/api/urlPresets';
|
import { postUrlPresets } from '../../../../common/api/urlPresets';
|
||||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import Button from '../../../../common/components/buttons/Button';
|
|
||||||
import IconButton from '../../../../common/components/buttons/IconButton';
|
|
||||||
import TooltipActionBtn from '../../../../common/components/buttons/TooltipActionBtn';
|
import TooltipActionBtn from '../../../../common/components/buttons/TooltipActionBtn';
|
||||||
import Info from '../../../../common/components/info/Info';
|
import Info from '../../../../common/components/info/Info';
|
||||||
import Input from '../../../../common/components/input/input/Input';
|
|
||||||
import ExternalLink from '../../../../common/components/link/external-link/ExternalLink';
|
import ExternalLink from '../../../../common/components/link/external-link/ExternalLink';
|
||||||
import useUrlPresets from '../../../../common/hooks-query/useUrlPresets';
|
import useUrlPresets from '../../../../common/hooks-query/useUrlPresets';
|
||||||
import { preventEscape } from '../../../../common/utils/keyEvent';
|
import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||||
@@ -18,7 +15,7 @@ import { handleLinks } from '../../../../common/utils/linkUtils';
|
|||||||
import { validateUrlPresetPath } from '../../../../common/utils/urlPresets';
|
import { validateUrlPresetPath } from '../../../../common/utils/urlPresets';
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import style from './FeaturePanel.module.scss';
|
import style from './FeatureSettings.module.scss';
|
||||||
|
|
||||||
const urlPresetsDocs = 'https://docs.getontime.no/features/url-presets/';
|
const urlPresetsDocs = 'https://docs.getontime.no/features/url-presets/';
|
||||||
|
|
||||||
@@ -101,10 +98,10 @@ export default function UrlPresetsForm() {
|
|||||||
<Panel.SubHeader>
|
<Panel.SubHeader>
|
||||||
URL presets
|
URL presets
|
||||||
<Panel.InlineElements>
|
<Panel.InlineElements>
|
||||||
<Button variant='ghosted' onClick={onReset} disabled={!canSubmit}>
|
<Button variant='ontime-ghosted' size='sm' onClick={onReset} isDisabled={!canSubmit}>
|
||||||
Revert to saved
|
Revert to saved
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant='primary' type='submit' disabled={!canSubmit} loading={isSubmitting}>
|
<Button variant='ontime-filled' size='sm' type='submit' isDisabled={!canSubmit} isLoading={isSubmitting}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
@@ -137,8 +134,8 @@ export default function UrlPresetsForm() {
|
|||||||
<Panel.Loader isLoading={isLoading} />
|
<Panel.Loader isLoading={isLoading} />
|
||||||
<Panel.Title>
|
<Panel.Title>
|
||||||
Manage presets
|
Manage presets
|
||||||
<Button onClick={addNew}>
|
<Button variant='ontime-subtle' rightIcon={<IoAdd />} size='sm' onClick={addNew}>
|
||||||
New <IoAdd />
|
New
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.Title>
|
</Panel.Title>
|
||||||
{errors?.root && <Panel.Error>{errors.root.message}</Panel.Error>}
|
{errors?.root && <Panel.Error>{errors.root.message}</Panel.Error>}
|
||||||
@@ -176,9 +173,11 @@ export default function UrlPresetsForm() {
|
|||||||
{...register(`data.${index}.alias`, {
|
{...register(`data.${index}.alias`, {
|
||||||
required: { value: true, message: 'Required field' },
|
required: { value: true, message: 'Required field' },
|
||||||
})}
|
})}
|
||||||
fluid
|
size='sm'
|
||||||
|
variant='ontime-filled'
|
||||||
placeholder='URL Preset'
|
placeholder='URL Preset'
|
||||||
data-testid={`field__alias_${index}`}
|
data-testid={`field__alias_${index}`}
|
||||||
|
autoComplete='off'
|
||||||
/>
|
/>
|
||||||
<Panel.Error>{maybeAliasError}</Panel.Error>
|
<Panel.Error>{maybeAliasError}</Panel.Error>
|
||||||
</td>
|
</td>
|
||||||
@@ -187,9 +186,11 @@ export default function UrlPresetsForm() {
|
|||||||
{...register(`data.${index}.pathAndParams`, {
|
{...register(`data.${index}.pathAndParams`, {
|
||||||
required: { value: true, message: 'Required field' },
|
required: { value: true, message: 'Required field' },
|
||||||
})}
|
})}
|
||||||
fluid
|
size='sm'
|
||||||
|
variant='ontime-filled'
|
||||||
placeholder='URL (portion after ontime Port)'
|
placeholder='URL (portion after ontime Port)'
|
||||||
data-testid={`field__url_${index}`}
|
data-testid={`field__url_${index}`}
|
||||||
|
autoComplete='off'
|
||||||
/>
|
/>
|
||||||
<Panel.Error>{maybeUrlError}</Panel.Error>
|
<Panel.Error>{maybeUrlError}</Panel.Error>
|
||||||
</td>
|
</td>
|
||||||
@@ -206,13 +207,14 @@ export default function UrlPresetsForm() {
|
|||||||
data-testid={`field__test_${index}`}
|
data-testid={`field__test_${index}`}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
size='sm'
|
||||||
onClick={() => remove(index)}
|
onClick={() => remove(index)}
|
||||||
variant='ghosted-destructive'
|
variant='ontime-ghosted'
|
||||||
|
color='#FA5656' // $red-500
|
||||||
|
icon={<IoTrash />}
|
||||||
aria-label='Delete entry'
|
aria-label='Delete entry'
|
||||||
data-testid={`field__delete_${index}`}
|
data-testid={`field__delete_${index}`}
|
||||||
>
|
/>
|
||||||
<IoTrash />
|
|
||||||
</IconButton>
|
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
+22
-22
@@ -1,8 +1,8 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { IoArrowDown, IoArrowUp, IoPencil, IoTrash } from 'react-icons/io5';
|
import { IoPencil, IoTrash } from 'react-icons/io5';
|
||||||
|
import { IconButton } from '@chakra-ui/react';
|
||||||
import { CustomField, CustomFieldKey } from 'ontime-types';
|
import { CustomField, CustomFieldKey } from 'ontime-types';
|
||||||
|
|
||||||
import IconButton from '../../../../../common/components/buttons/IconButton';
|
|
||||||
import CopyTag from '../../../../../common/components/copy-tag/CopyTag';
|
import CopyTag from '../../../../../common/components/copy-tag/CopyTag';
|
||||||
import Swatch from '../../../../../common/components/input/colour-input/Swatch';
|
import Swatch from '../../../../../common/components/input/colour-input/Swatch';
|
||||||
import Tag from '../../../../../common/components/tag/Tag';
|
import Tag from '../../../../../common/components/tag/Tag';
|
||||||
@@ -10,26 +10,22 @@ import * as Panel from '../../../panel-utils/PanelUtils';
|
|||||||
|
|
||||||
import CustomFieldForm from './CustomFieldForm';
|
import CustomFieldForm from './CustomFieldForm';
|
||||||
|
|
||||||
import style from '../ManagePanel.module.scss';
|
import style from '../FeatureSettings.module.scss';
|
||||||
|
|
||||||
interface CustomFieldEntryProps {
|
interface CustomFieldEntryProps {
|
||||||
colour: string;
|
colour: string;
|
||||||
label: string;
|
label: string;
|
||||||
fieldKey: string;
|
fieldKey: string;
|
||||||
type: 'string' | 'image';
|
type: 'string' | 'image';
|
||||||
order?: number; // Add order
|
onEdit: (key: CustomFieldKey, patch: CustomField) => Promise<void>;
|
||||||
onEdit: (key: CustomFieldKey, patch: Partial<CustomField>) => Promise<void>; // Patch can be partial for order updates
|
|
||||||
onDelete: (key: CustomFieldKey) => Promise<void>;
|
onDelete: (key: CustomFieldKey) => Promise<void>;
|
||||||
isFirst: boolean;
|
|
||||||
isLast: boolean;
|
|
||||||
onMove: (direction: 'up' | 'down') => void; // Changed from Promise<void> to void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CustomFieldEntry(props: CustomFieldEntryProps) {
|
export default function CustomFieldEntry(props: CustomFieldEntryProps) {
|
||||||
const { colour, label, fieldKey, type, onEdit, onDelete, isFirst, isLast, onMove } = props;
|
const { colour, label, fieldKey, type, onEdit, onDelete } = props;
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
const handleEdit = async (patch: CustomField) => { // This patch comes from CustomFieldForm, so it's a full CustomField
|
const handleEdit = async (patch: CustomField) => {
|
||||||
await onEdit(fieldKey, patch);
|
await onEdit(fieldKey, patch);
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
};
|
};
|
||||||
@@ -65,18 +61,22 @@ export default function CustomFieldEntry(props: CustomFieldEntryProps) {
|
|||||||
</CopyTag>
|
</CopyTag>
|
||||||
</td>
|
</td>
|
||||||
<Panel.InlineElements relation='inner' as='td'>
|
<Panel.InlineElements relation='inner' as='td'>
|
||||||
<IconButton variant='ghosted-white' aria-label='Move field up' onClick={() => onMove('up')} disabled={isFirst}>
|
<IconButton
|
||||||
<IoArrowUp />
|
size='sm'
|
||||||
</IconButton>
|
variant='ontime-ghosted'
|
||||||
<IconButton variant='ghosted-white' aria-label='Move field down' onClick={() => onMove('down')} disabled={isLast}>
|
color='#e2e2e2' // $gray-200
|
||||||
<IoArrowDown />
|
icon={<IoPencil />}
|
||||||
</IconButton>
|
aria-label='Edit entry'
|
||||||
<IconButton variant='ghosted-white' aria-label='Edit entry' onClick={() => setIsEditing(true)}>
|
onClick={() => setIsEditing(true)}
|
||||||
<IoPencil />
|
/>
|
||||||
</IconButton>
|
<IconButton
|
||||||
<IconButton variant='ghosted-destructive' aria-label='Delete entry' onClick={() => onDelete(fieldKey)}>
|
size='sm'
|
||||||
<IoTrash />
|
variant='ontime-ghosted'
|
||||||
</IconButton>
|
color='#FA5656' // $red-500
|
||||||
|
icon={<IoTrash />}
|
||||||
|
aria-label='Delete entry'
|
||||||
|
onClick={() => onDelete(fieldKey)}
|
||||||
|
/>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
+8
-8
@@ -1,19 +1,17 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { Radio, RadioGroup } from '@chakra-ui/react';
|
import { Button, Input, Radio, RadioGroup } from '@chakra-ui/react';
|
||||||
import { CustomField } from 'ontime-types';
|
import { CustomField } from 'ontime-types';
|
||||||
import { customFieldLabelToKey, isAlphanumericWithSpace } from 'ontime-utils';
|
import { customFieldLabelToKey, isAlphanumericWithSpace } from 'ontime-utils';
|
||||||
|
|
||||||
import { maybeAxiosError } from '../../../../../common/api/utils';
|
import { maybeAxiosError } from '../../../../../common/api/utils';
|
||||||
import Button from '../../../../../common/components/buttons/Button';
|
|
||||||
import Info from '../../../../../common/components/info/Info';
|
import Info from '../../../../../common/components/info/Info';
|
||||||
import SwatchSelect from '../../../../../common/components/input/colour-input/SwatchSelect';
|
import SwatchSelect from '../../../../../common/components/input/colour-input/SwatchSelect';
|
||||||
import Input from '../../../../../common/components/input/input/Input';
|
|
||||||
import useCustomFields from '../../../../../common/hooks-query/useCustomFields';
|
import useCustomFields from '../../../../../common/hooks-query/useCustomFields';
|
||||||
import { preventEscape } from '../../../../../common/utils/keyEvent';
|
import { preventEscape } from '../../../../../common/utils/keyEvent';
|
||||||
import * as Panel from '../../../panel-utils/PanelUtils';
|
import * as Panel from '../../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import style from '../ManagePanel.module.scss';
|
import style from '../FeatureSettings.module.scss';
|
||||||
|
|
||||||
interface CustomFieldsFormProps {
|
interface CustomFieldsFormProps {
|
||||||
onSubmit: (field: CustomField) => Promise<void>;
|
onSubmit: (field: CustomField) => Promise<void>;
|
||||||
@@ -120,13 +118,15 @@ export default function CustomFieldForm(props: CustomFieldsFormProps) {
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
fluid
|
size='sm'
|
||||||
|
variant='ontime-filled'
|
||||||
|
autoComplete='off'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Panel.Description>Key (use in Integrations and API)</Panel.Description>
|
<Panel.Description>Key (use in Integrations and API)</Panel.Description>
|
||||||
<Input {...register('key')} readOnly fluid />
|
<Input {...register('key')} disabled size='sm' variant='ontime-filled' autoComplete='off' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -135,10 +135,10 @@ export default function CustomFieldForm(props: CustomFieldsFormProps) {
|
|||||||
</div>
|
</div>
|
||||||
{errors.root && <Panel.Error>{errors.root.message}</Panel.Error>}
|
{errors.root && <Panel.Error>{errors.root.message}</Panel.Error>}
|
||||||
<Panel.InlineElements relation='inner' align='end'>
|
<Panel.InlineElements relation='inner' align='end'>
|
||||||
<Button variant='ghosted' onClick={onCancel}>
|
<Button size='sm' variant='ontime-ghosted' onClick={onCancel}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type='submit' variant='primary' disabled={!canSubmit} loading={isSubmitting}>
|
<Button size='sm' type='submit' variant='ontime-filled' isDisabled={!canSubmit} isLoading={isSubmitting}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { IoAdd } from 'react-icons/io5';
|
||||||
|
import { Button } from '@chakra-ui/react';
|
||||||
|
import { CustomField, CustomFieldKey } from 'ontime-types';
|
||||||
|
|
||||||
|
import { deleteCustomField, editCustomField, postCustomField } from '../../../../../common/api/customFields';
|
||||||
|
import Info from '../../../../../common/components/info/Info';
|
||||||
|
import ExternalLink from '../../../../../common/components/link/external-link/ExternalLink';
|
||||||
|
import useCustomFields from '../../../../../common/hooks-query/useCustomFields';
|
||||||
|
import { customFieldsDocsUrl } from '../../../../../externals';
|
||||||
|
import * as Panel from '../../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
|
import CustomFieldEntry from './CustomFieldEntry';
|
||||||
|
import CustomFieldForm from './CustomFieldForm';
|
||||||
|
|
||||||
|
export default function CustomFields() {
|
||||||
|
const { data, refetch } = useCustomFields();
|
||||||
|
const [isAdding, setIsAdding] = useState(false);
|
||||||
|
|
||||||
|
const handleInitiateCreate = () => {
|
||||||
|
setIsAdding(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setIsAdding(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreate = async (customField: CustomField) => {
|
||||||
|
await postCustomField(customField);
|
||||||
|
refetch();
|
||||||
|
setIsAdding(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditField = async (key: CustomFieldKey, customField: CustomField) => {
|
||||||
|
await editCustomField(key, customField);
|
||||||
|
refetch();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (key: CustomFieldKey) => {
|
||||||
|
try {
|
||||||
|
await deleteCustomField(key);
|
||||||
|
refetch();
|
||||||
|
} catch (_error) {
|
||||||
|
/** we do not handle errors here */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel.Section>
|
||||||
|
<Panel.Card>
|
||||||
|
<Panel.SubHeader>
|
||||||
|
Custom fields
|
||||||
|
<Button variant='ontime-subtle' rightIcon={<IoAdd />} size='sm' onClick={handleInitiateCreate}>
|
||||||
|
New
|
||||||
|
</Button>
|
||||||
|
</Panel.SubHeader>
|
||||||
|
<Panel.Divider />
|
||||||
|
<Panel.Section>
|
||||||
|
<Info>
|
||||||
|
Custom fields allow for additional information to be added to an event.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
This data is not used by Ontime, but provides place for cueing or department specific information (eg.
|
||||||
|
light, sound, camera).
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
Custom fields can be used width the Integrations feature using the generated key.
|
||||||
|
<ExternalLink href={customFieldsDocsUrl}>See the docs</ExternalLink>
|
||||||
|
</Info>
|
||||||
|
</Panel.Section>
|
||||||
|
<Panel.Section>
|
||||||
|
{isAdding && <CustomFieldForm onSubmit={handleCreate} onCancel={handleCancel} />}
|
||||||
|
<Panel.Table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Colour</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Key (used in Integrations)</th>
|
||||||
|
<th />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Object.entries(data).map(([key, { colour, label, type }]) => {
|
||||||
|
return (
|
||||||
|
<CustomFieldEntry
|
||||||
|
key={key}
|
||||||
|
fieldKey={key}
|
||||||
|
colour={colour}
|
||||||
|
label={label}
|
||||||
|
type={type}
|
||||||
|
onEdit={handleEditField}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</Panel.Table>
|
||||||
|
</Panel.Section>
|
||||||
|
</Panel.Card>
|
||||||
|
</Panel.Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||||
|
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
||||||
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
import EditorSettingsForm from '../interface-panel/EditorSettingsForm';
|
||||||
|
|
||||||
|
import GeneralPanelForm from './GeneralPanelForm';
|
||||||
|
import ViewSettingsForm from './ViewSettingsForm';
|
||||||
|
|
||||||
|
export default function GeneralPanel({ location }: PanelBaseProps) {
|
||||||
|
const generalRef = useScrollIntoView<HTMLDivElement>('settings', location);
|
||||||
|
const editorRef = useScrollIntoView<HTMLDivElement>('editor', location);
|
||||||
|
const viewRef = useScrollIntoView<HTMLDivElement>('view', location);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Panel.Header>App Settings</Panel.Header>
|
||||||
|
<div ref={generalRef}>
|
||||||
|
<GeneralPanelForm />
|
||||||
|
</div>
|
||||||
|
<div ref={editorRef}>
|
||||||
|
<EditorSettingsForm />
|
||||||
|
</div>
|
||||||
|
<div ref={viewRef}>
|
||||||
|
<ViewSettingsForm />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
+16
-9
@@ -1,21 +1,19 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { Select } from '@chakra-ui/react';
|
import { Button, Input, Select } from '@chakra-ui/react';
|
||||||
import { Settings } from 'ontime-types';
|
import { Settings } from 'ontime-types';
|
||||||
|
|
||||||
import { postSettings } from '../../../../common/api/settings';
|
import { postSettings } from '../../../../common/api/settings';
|
||||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import Button from '../../../../common/components/buttons/Button';
|
|
||||||
import Input from '../../../../common/components/input/input/Input';
|
|
||||||
import useSettings from '../../../../common/hooks-query/useSettings';
|
import useSettings from '../../../../common/hooks-query/useSettings';
|
||||||
import { preventEscape } from '../../../../common/utils/keyEvent';
|
import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||||
import { isOnlyNumbers } from '../../../../common/utils/regex';
|
import { isOnlyNumbers } from '../../../../common/utils/regex';
|
||||||
import { isOntimeCloud } from '../../../../externals';
|
import { isOntimeCloud } from '../../../../externals';
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import GeneralPinInput from './composite/GeneralPinInput';
|
import GeneralPinInput from './GeneralPinInput';
|
||||||
|
|
||||||
export default function GeneralSettings() {
|
export default function GeneralPanelForm() {
|
||||||
const { data, status, refetch } = useSettings();
|
const { data, status, refetch } = useSettings();
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -71,10 +69,17 @@ export default function GeneralSettings() {
|
|||||||
<Panel.SubHeader>
|
<Panel.SubHeader>
|
||||||
General settings
|
General settings
|
||||||
<Panel.InlineElements>
|
<Panel.InlineElements>
|
||||||
<Button disabled={!isDirty || isSubmitting} variant='ghosted' onClick={onReset}>
|
<Button isDisabled={!isDirty || isSubmitting} variant='ontime-ghosted' size='sm' onClick={onReset}>
|
||||||
Revert to saved
|
Revert to saved
|
||||||
</Button>
|
</Button>
|
||||||
<Button type='submit' form='app-settings' loading={isSubmitting} disabled={disableSubmit} variant='primary'>
|
<Button
|
||||||
|
type='submit'
|
||||||
|
form='app-settings'
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={disableSubmit}
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
@@ -96,10 +101,12 @@ export default function GeneralSettings() {
|
|||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
id='serverPort'
|
id='serverPort'
|
||||||
|
size='sm'
|
||||||
type='number'
|
type='number'
|
||||||
|
variant='ontime-filled'
|
||||||
maxLength={5}
|
maxLength={5}
|
||||||
style={{ width: '75px' }}
|
width='75px'
|
||||||
disabled={isOntimeCloud}
|
isDisabled={isOntimeCloud}
|
||||||
{...register('serverPort', {
|
{...register('serverPort', {
|
||||||
required: { value: true, message: 'Required field' },
|
required: { value: true, message: 'Required field' },
|
||||||
max: { value: 65535, message: 'Port must be within range 1024 - 65535' },
|
max: { value: 65535, message: 'Port must be within range 1024 - 65535' },
|
||||||
+1
-1
@@ -4,7 +4,7 @@ import { IoEyeOutline } from 'react-icons/io5';
|
|||||||
import { IconButton, Input, InputGroup, InputRightElement } from '@chakra-ui/react';
|
import { IconButton, Input, InputGroup, InputRightElement } from '@chakra-ui/react';
|
||||||
import { Settings } from 'ontime-types';
|
import { Settings } from 'ontime-types';
|
||||||
|
|
||||||
import { isAlphanumeric } from '../../../../../common/utils/regex';
|
import { isAlphanumeric } from '../../../../common/utils/regex';
|
||||||
|
|
||||||
interface GeneralPinInputProps {
|
interface GeneralPinInputProps {
|
||||||
register: UseFormRegister<Settings>;
|
register: UseFormRegister<Settings>;
|
||||||
-3
@@ -8,8 +8,5 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.column {
|
.column {
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
+40
-27
@@ -1,10 +1,18 @@
|
|||||||
import { lazy, useEffect, useRef, useState } from 'react';
|
import { lazy, useEffect, useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
import { getCSSContents, postCSSContents, restoreCSSContents } from '../../../../../common/api/assets';
|
import { getCSSContents, postCSSContents, restoreCSSContents } from '../../../../common/api/assets';
|
||||||
import Button from '../../../../../common/components/buttons/Button';
|
import Info from '../../../../common/components/info/Info';
|
||||||
import Info from '../../../../../common/components/info/Info';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
import Modal from '../../../../../common/components/modal/Modal';
|
|
||||||
import * as Panel from '../../../panel-utils/PanelUtils';
|
|
||||||
|
|
||||||
import style from './StyleEditorModal.module.scss';
|
import style from './StyleEditorModal.module.scss';
|
||||||
|
|
||||||
@@ -19,7 +27,9 @@ interface CSSRef {
|
|||||||
getCss: () => string;
|
getCss: () => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CodeEditorModal({ isOpen, onClose }: CodeEditorModalProps) {
|
export default function CodeEditorModal(props: CodeEditorModalProps) {
|
||||||
|
const { isOpen, onClose } = props;
|
||||||
|
|
||||||
const [css, setCSS] = useState('');
|
const [css, setCSS] = useState('');
|
||||||
const [isDirty, setIsDirty] = useState(false);
|
const [isDirty, setIsDirty] = useState(false);
|
||||||
const [saveLoading, setSaveLoading] = useState(false);
|
const [saveLoading, setSaveLoading] = useState(false);
|
||||||
@@ -74,43 +84,46 @@ export default function CodeEditorModal({ isOpen, onClose }: CodeEditorModalProp
|
|||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal isOpen={isOpen} onClose={onClose} variant='ontime' isCentered>
|
||||||
title='Edit CSS override'
|
<ModalOverlay />
|
||||||
isOpen={isOpen}
|
<ModalContent maxWidth='max(800px, 40vw)'>
|
||||||
onClose={onClose}
|
<ModalHeader>Edit CSS override</ModalHeader>
|
||||||
showCloseButton
|
<ModalCloseButton />
|
||||||
showBackdrop
|
<ModalBody>
|
||||||
bodyElements={
|
<CodeEditor ref={cssRef} initialValue={css} language='css' isDirty={isDirty} setIsDirty={setIsDirty} />
|
||||||
<CodeEditor ref={cssRef} initialValue={css} language='css' isDirty={isDirty} setIsDirty={setIsDirty} />
|
</ModalBody>
|
||||||
}
|
|
||||||
footerElements={
|
<ModalFooter className={style.column}>
|
||||||
<div className={style.column}>
|
|
||||||
<Info>Invalid CSS will be refused by the browser</Info>
|
<Info>Invalid CSS will be refused by the browser</Info>
|
||||||
{error && <Panel.Error className={style.right}>{`Error: ${error}`}</Panel.Error>}
|
{error && <Panel.Error className={style.right}>{`Error: ${error}`}</Panel.Error>}
|
||||||
<Panel.InlineElements align='apart' className={style.editorActions}>
|
<Panel.InlineElements align='apart' className={style.editorActions}>
|
||||||
<Button variant='ghosted' size='large' onClick={handleRestore} disabled={saveLoading || resetLoading}>
|
<Button
|
||||||
|
variant='ontime-ghosted'
|
||||||
|
onClick={handleRestore}
|
||||||
|
isDisabled={saveLoading || resetLoading}
|
||||||
|
isLoading={resetLoading}
|
||||||
|
>
|
||||||
Reset to example
|
Reset to example
|
||||||
</Button>
|
</Button>
|
||||||
<Panel.InlineElements>
|
<Panel.InlineElements>
|
||||||
<Button variant='ghosted' size='large' onClick={clear}>
|
<Button variant='ontime-ghosted' onClick={clear}>
|
||||||
Clear
|
Clear
|
||||||
</Button>
|
</Button>
|
||||||
<Button size='large' onClick={onClose}>
|
<Button variant='ontime-subtle' onClick={onClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant='primary'
|
variant='ontime-filled'
|
||||||
size='large'
|
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={saveLoading || resetLoading || !isDirty}
|
isDisabled={saveLoading || resetLoading || !isDirty}
|
||||||
loading={saveLoading}
|
isLoading={saveLoading}
|
||||||
>
|
>
|
||||||
Save changes
|
Save changes
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
</div>
|
</ModalFooter>
|
||||||
}
|
</ModalContent>
|
||||||
/>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+36
-17
@@ -1,26 +1,26 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { Switch } from '@chakra-ui/react';
|
import { Button, Input, Switch, useDisclosure } from '@chakra-ui/react';
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { ViewSettings } from 'ontime-types';
|
||||||
import { ViewSettings as ViewSettingsType } from 'ontime-types';
|
|
||||||
|
|
||||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import Button from '../../../../common/components/buttons/Button';
|
|
||||||
import Info from '../../../../common/components/info/Info';
|
import Info from '../../../../common/components/info/Info';
|
||||||
import { SwatchPickerRHF } from '../../../../common/components/input/colour-input/SwatchPicker';
|
import { SwatchPickerRHF } from '../../../../common/components/input/colour-input/SwatchPicker';
|
||||||
import Input from '../../../../common/components/input/input/Input';
|
|
||||||
import ExternalLink from '../../../../common/components/link/external-link/ExternalLink';
|
import ExternalLink from '../../../../common/components/link/external-link/ExternalLink';
|
||||||
|
import useInfo from '../../../../common/hooks-query/useInfo';
|
||||||
import useViewSettings from '../../../../common/hooks-query/useViewSettings';
|
import useViewSettings from '../../../../common/hooks-query/useViewSettings';
|
||||||
import { preventEscape } from '../../../../common/utils/keyEvent';
|
import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||||
|
import { isOntimeCloud } from '../../../../externals';
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import CodeEditorModal from './composite/StyleEditorModal';
|
import CodeEditorModal from './StyleEditorModal';
|
||||||
|
|
||||||
const cssOverrideDocsUrl = 'https://docs.getontime.no/features/custom-styling/';
|
const cssOverrideDocsUrl = 'https://docs.getontime.no/features/custom-styling/';
|
||||||
|
|
||||||
export default function ViewSettings() {
|
export default function ViewSettingsForm() {
|
||||||
const { data, isPending, mutateAsync } = useViewSettings();
|
const { data, isPending, mutateAsync } = useViewSettings();
|
||||||
const [isCodeEditorOpen, codeEditorHandler] = useDisclosure();
|
const { data: info, status: infoStatus } = useInfo();
|
||||||
|
const { isOpen: isCodeEditorOpen, onOpen: onCodeEditorOpen, onClose: onCodeEditorClose } = useDisclosure();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
@@ -29,7 +29,7 @@ export default function ViewSettings() {
|
|||||||
register,
|
register,
|
||||||
reset,
|
reset,
|
||||||
formState: { isSubmitting, isDirty, errors },
|
formState: { isSubmitting, isDirty, errors },
|
||||||
} = useForm<ViewSettingsType>({
|
} = useForm<ViewSettings>({
|
||||||
defaultValues: data,
|
defaultValues: data,
|
||||||
values: data,
|
values: data,
|
||||||
resetOptions: {
|
resetOptions: {
|
||||||
@@ -44,7 +44,7 @@ export default function ViewSettings() {
|
|||||||
}
|
}
|
||||||
}, [data, reset]);
|
}, [data, reset]);
|
||||||
|
|
||||||
const onSubmit = async (formData: ViewSettingsType) => {
|
const onSubmit = async (formData: ViewSettings) => {
|
||||||
try {
|
try {
|
||||||
mutateAsync(formData);
|
mutateAsync(formData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -61,6 +61,8 @@ export default function ViewSettings() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isLoading = isPending || infoStatus === 'pending';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel.Section
|
<Panel.Section
|
||||||
as='form'
|
as='form'
|
||||||
@@ -72,25 +74,33 @@ export default function ViewSettings() {
|
|||||||
<Panel.SubHeader>
|
<Panel.SubHeader>
|
||||||
View settings
|
View settings
|
||||||
<Panel.InlineElements>
|
<Panel.InlineElements>
|
||||||
<Button disabled={!isDirty} variant='ghosted' onClick={onReset}>
|
<Button isDisabled={!isDirty} variant='ontime-ghosted' size='sm' onClick={onReset}>
|
||||||
Revert to saved
|
Revert to saved
|
||||||
</Button>
|
</Button>
|
||||||
<Button type='submit' loading={isSubmitting} disabled={!isDirty} variant='primary'>
|
<Button type='submit' isLoading={isSubmitting} isDisabled={!isDirty} variant='ontime-filled' size='sm'>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
</Panel.SubHeader>
|
</Panel.SubHeader>
|
||||||
<Panel.Divider />
|
<Panel.Divider />
|
||||||
<Info>
|
<Info>
|
||||||
You can customise the styles applied to Ontime views by providing overriding CSS rules.
|
You can the Ontime views or customise its styles by modifying the provided CSS file.
|
||||||
<br />
|
<br />
|
||||||
|
{!isOntimeCloud && (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
The loaded CSS file is in the user directory at{' '}
|
||||||
|
<Panel.BlockQuote>{`${info.publicDir}/user/styles/override.css`}</Panel.BlockQuote>
|
||||||
|
<br />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<ExternalLink href={cssOverrideDocsUrl}>See the docs</ExternalLink>
|
<ExternalLink href={cssOverrideDocsUrl}>See the docs</ExternalLink>
|
||||||
</Info>
|
</Info>
|
||||||
<Panel.Section>
|
<Panel.Section>
|
||||||
<Panel.Loader isLoading={isPending} />
|
<Panel.Loader isLoading={isLoading} />
|
||||||
<Panel.Error>{errors.root?.message}</Panel.Error>
|
<Panel.Error>{errors.root?.message}</Panel.Error>
|
||||||
<Panel.ListGroup>
|
<Panel.ListGroup>
|
||||||
<CodeEditorModal isOpen={isCodeEditorOpen} onClose={codeEditorHandler.close} />
|
<CodeEditorModal isOpen={isCodeEditorOpen} onClose={onCodeEditorClose} />
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field
|
<Panel.Field
|
||||||
title='Override CSS styles'
|
title='Override CSS styles'
|
||||||
@@ -103,7 +113,13 @@ export default function ViewSettings() {
|
|||||||
<Switch variant='ontime' size='lg' isChecked={value} onChange={onChange} ref={ref} />
|
<Switch variant='ontime' size='lg' isChecked={value} onChange={onChange} ref={ref} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Button onClick={codeEditorHandler.open} disabled={isSubmitting}>
|
<Button
|
||||||
|
onClick={onCodeEditorOpen}
|
||||||
|
variant='ontime-subtle'
|
||||||
|
size='sm'
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
width='fit-content'
|
||||||
|
>
|
||||||
Edit CSS override
|
Edit CSS override
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
@@ -142,8 +158,11 @@ export default function ViewSettings() {
|
|||||||
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'
|
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'
|
||||||
|
autoComplete='off'
|
||||||
|
variant='ontime-filled'
|
||||||
maxLength={150}
|
maxLength={150}
|
||||||
style={{ width: '275px' }}
|
width='275px'
|
||||||
placeholder='Shown when timer reaches end'
|
placeholder='Shown when timer reaches end'
|
||||||
{...register('endMessage')}
|
{...register('endMessage')}
|
||||||
/>
|
/>
|
||||||
+41
-3
@@ -6,7 +6,7 @@ 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 '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
export default function RundownDefaultSettings() {
|
export default function EditorSettingsForm() {
|
||||||
const {
|
const {
|
||||||
defaultDuration,
|
defaultDuration,
|
||||||
linkPrevious,
|
linkPrevious,
|
||||||
@@ -31,10 +31,10 @@ export default function RundownDefaultSettings() {
|
|||||||
return (
|
return (
|
||||||
<Panel.Section>
|
<Panel.Section>
|
||||||
<Panel.Card>
|
<Panel.Card>
|
||||||
<Panel.SubHeader>Rundown defaults</Panel.SubHeader>
|
<Panel.SubHeader>Editor settings</Panel.SubHeader>
|
||||||
<Panel.Divider />
|
<Panel.Divider />
|
||||||
<Panel.Section>
|
<Panel.Section>
|
||||||
<Panel.Title>Default settings for new events</Panel.Title>
|
<Panel.Title>Rundown defaults for new events</Panel.Title>
|
||||||
<Panel.ListGroup>
|
<Panel.ListGroup>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field
|
<Panel.Field
|
||||||
@@ -126,6 +126,44 @@ export default function RundownDefaultSettings() {
|
|||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
</Panel.ListGroup>
|
</Panel.ListGroup>
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
|
<Panel.Section>
|
||||||
|
<Panel.Title>Run mode</Panel.Title>
|
||||||
|
<Panel.ListGroup>
|
||||||
|
<Panel.ListItem>
|
||||||
|
<Panel.Field
|
||||||
|
title='Show quick entry'
|
||||||
|
description='Whether the quick entry buttons show above / under selected event'
|
||||||
|
/>
|
||||||
|
<Switch variant='ontime' size='lg' defaultChecked={false} isDisabled />
|
||||||
|
</Panel.ListItem>
|
||||||
|
<Panel.ListItem>
|
||||||
|
<Panel.Field
|
||||||
|
title='Follow playback'
|
||||||
|
description='Whether view automatically follows the event being played'
|
||||||
|
/>
|
||||||
|
<Switch variant='ontime' size='lg' defaultChecked isDisabled />
|
||||||
|
</Panel.ListItem>
|
||||||
|
</Panel.ListGroup>
|
||||||
|
</Panel.Section>
|
||||||
|
<Panel.Section>
|
||||||
|
<Panel.Title>Edit mode</Panel.Title>
|
||||||
|
<Panel.ListGroup>
|
||||||
|
<Panel.ListItem>
|
||||||
|
<Panel.Field
|
||||||
|
title='Show quick entry'
|
||||||
|
description='Whether the quick entry buttons show above / under selected event'
|
||||||
|
/>
|
||||||
|
<Switch variant='ontime' size='lg' defaultChecked isDisabled />
|
||||||
|
</Panel.ListItem>
|
||||||
|
<Panel.ListItem>
|
||||||
|
<Panel.Field
|
||||||
|
title='Follow playback'
|
||||||
|
description='Whether view automatically follows the event being played'
|
||||||
|
/>
|
||||||
|
<Switch variant='ontime' size='lg' defaultChecked={false} isDisabled />
|
||||||
|
</Panel.ListItem>
|
||||||
|
</Panel.ListGroup>
|
||||||
|
</Panel.Section>
|
||||||
</Panel.Card>
|
</Panel.Card>
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
);
|
);
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react'; // Import useEffect
|
|
||||||
import { IoAdd } from 'react-icons/io5';
|
|
||||||
import { CustomField, CustomFieldKey } from 'ontime-types';
|
|
||||||
|
|
||||||
// Import CustomFieldWithKey
|
|
||||||
import { deleteCustomField, editCustomField, postCustomField, CustomFieldWithKey } from '../../../../common/api/customFields';
|
|
||||||
import Button from '../../../../common/components/buttons/Button';
|
|
||||||
import Info from '../../../../common/components/info/Info';
|
|
||||||
import ExternalLink from '../../../../common/components/link/external-link/ExternalLink';
|
|
||||||
import useCustomFields from '../../../../common/hooks-query/useCustomFields';
|
|
||||||
import { customFieldsDocsUrl } from '../../../../externals';
|
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
|
||||||
|
|
||||||
import CustomFieldEntry from './composite/CustomFieldEntry';
|
|
||||||
import CustomFieldForm from './composite/CustomFieldForm';
|
|
||||||
|
|
||||||
export default function CustomFieldSettings() {
|
|
||||||
const { data, refetch } = useCustomFields(); // data is CustomFieldWithKey[]
|
|
||||||
const [isAdding, setIsAdding] = useState(false);
|
|
||||||
const [displayedFields, setDisplayedFields] = useState<CustomFieldWithKey[]>([]);
|
|
||||||
const [orderChanged, setOrderChanged] = useState(false); // To track if order has changed for enabling Save button
|
|
||||||
const [isSavingOrder, setIsSavingOrder] = useState(false); // For Save Order button loading state
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Initialize displayedFields with fetched data, ensuring a fresh copy for local manipulation
|
|
||||||
// And sort it initially, as the backend already provides it sorted, but this ensures consistency.
|
|
||||||
setDisplayedFields(data ? [...data].sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity)) : []);
|
|
||||||
// When data is refetched from server (e.g. after save), reset orderChanged flag
|
|
||||||
setOrderChanged(false);
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const handleSaveOrder = async () => {
|
|
||||||
setIsSavingOrder(true);
|
|
||||||
const updatePromises = [];
|
|
||||||
|
|
||||||
// Create a map of original orders for quick lookup
|
|
||||||
const originalFieldsMap = new Map(data.map(f => [f.key, f]));
|
|
||||||
|
|
||||||
for (let i = 0; i < displayedFields.length; i++) {
|
|
||||||
const currentField = displayedFields[i];
|
|
||||||
const originalField = originalFieldsMap.get(currentField.key);
|
|
||||||
|
|
||||||
// The new order is its current index in the displayedFields array
|
|
||||||
const newOrder = i;
|
|
||||||
|
|
||||||
// Check if the effective order has changed, or if it's a new field without an original order yet (though create handles initial order)
|
|
||||||
// or if the field itself is new and not in originalFieldsMap (less likely here as it should have been created)
|
|
||||||
if (!originalField || originalField.order !== newOrder) {
|
|
||||||
// Only update if the order property is actually different
|
|
||||||
// We need to ensure the object passed to editCustomField has all required CustomField props
|
|
||||||
// and the order. The 'key' is passed as the first argument to editCustomField.
|
|
||||||
const fieldToSave: Partial<CustomField> = {
|
|
||||||
label: currentField.label,
|
|
||||||
type: currentField.type,
|
|
||||||
colour: currentField.colour,
|
|
||||||
order: newOrder,
|
|
||||||
};
|
|
||||||
updatePromises.push(editCustomField(currentField.key, fieldToSave));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Promise.all(updatePromises);
|
|
||||||
setOrderChanged(false);
|
|
||||||
refetch(); // Refetch data from the server to confirm and get fresh state
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error saving custom field order:", error);
|
|
||||||
// Potentially show an error message to the user
|
|
||||||
} finally {
|
|
||||||
setIsSavingOrder(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInitiateCreate = () => {
|
|
||||||
setIsAdding(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setIsAdding(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreate = async (customField: CustomField) => {
|
|
||||||
await postCustomField(customField);
|
|
||||||
refetch();
|
|
||||||
setIsAdding(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditField = async (key: CustomFieldKey, customField: CustomField) => {
|
|
||||||
await editCustomField(key, customField);
|
|
||||||
refetch();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async (key: CustomFieldKey) => {
|
|
||||||
try {
|
|
||||||
await deleteCustomField(key);
|
|
||||||
refetch();
|
|
||||||
} catch (_error) {
|
|
||||||
/** we do not handle errors here */
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Panel.Section>
|
|
||||||
<Panel.Card>
|
|
||||||
<Panel.SubHeader>
|
|
||||||
Custom fields
|
|
||||||
<Panel.InlineElements>
|
|
||||||
<Button onClick={handleSaveOrder} variant='primary' disabled={!orderChanged || isSavingOrder} loading={isSavingOrder}>
|
|
||||||
Save Order
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleInitiateCreate} disabled={isSavingOrder}>
|
|
||||||
New <IoAdd />
|
|
||||||
</Button>
|
|
||||||
</Panel.InlineElements>
|
|
||||||
</Panel.SubHeader>
|
|
||||||
<Panel.Divider />
|
|
||||||
<Panel.Section>
|
|
||||||
<Info>
|
|
||||||
Custom fields allow for additional information to be added to an event.
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
This data can be used in the Automation feature by using the generated key.
|
|
||||||
<ExternalLink href={customFieldsDocsUrl}>See the docs</ExternalLink>
|
|
||||||
</Info>
|
|
||||||
</Panel.Section>
|
|
||||||
<Panel.Section>
|
|
||||||
{isAdding && <CustomFieldForm onSubmit={handleCreate} onCancel={handleCancel} />}
|
|
||||||
<Panel.Table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Colour</th>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Key (used in Integrations)</th>
|
|
||||||
<th />
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{displayedFields.map((field, index) => { // Iterate over displayedFields
|
|
||||||
return (
|
|
||||||
<CustomFieldEntry
|
|
||||||
key={field.key}
|
|
||||||
fieldKey={field.key}
|
|
||||||
colour={field.colour}
|
|
||||||
label={field.label}
|
|
||||||
type={field.type}
|
|
||||||
order={field.order} // Pass order
|
|
||||||
onEdit={handleEditField}
|
|
||||||
onDelete={handleDelete}
|
|
||||||
// For reordering
|
|
||||||
isFirst={index === 0}
|
|
||||||
isLast={index === displayedFields.length - 1} // Use displayedFields.length
|
|
||||||
onMove={(direction: 'up' | 'down') => {
|
|
||||||
const newFields = [...displayedFields];
|
|
||||||
const fieldToMove = newFields[index];
|
|
||||||
let neighborIndex = -1;
|
|
||||||
|
|
||||||
if (direction === 'up' && index > 0) {
|
|
||||||
neighborIndex = index - 1;
|
|
||||||
} else if (direction === 'down' && index < newFields.length - 1) {
|
|
||||||
neighborIndex = index + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (neighborIndex !== -1) {
|
|
||||||
const neighborField = newFields[neighborIndex];
|
|
||||||
|
|
||||||
// Swap order properties
|
|
||||||
const tempOrder = fieldToMove.order;
|
|
||||||
fieldToMove.order = neighborField.order;
|
|
||||||
neighborField.order = tempOrder;
|
|
||||||
|
|
||||||
// Actual swap in the array for immediate UI feedback before potential sort
|
|
||||||
newFields[index] = neighborField;
|
|
||||||
newFields[neighborIndex] = fieldToMove;
|
|
||||||
|
|
||||||
// Sort by the new order values to ensure dense packing if orders were sparse or undefined
|
|
||||||
// This also handles cases where initial orders might not be perfectly sequential.
|
|
||||||
newFields.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity));
|
|
||||||
|
|
||||||
// If orders became non-dense or undefined, re-assign dense orders
|
|
||||||
// This ensures that when we save, we send a clean, sequential order.
|
|
||||||
let orderIsDirty = false;
|
|
||||||
newFields.forEach((f, i) => {
|
|
||||||
if(f.order !== i) {
|
|
||||||
f.order = i;
|
|
||||||
orderIsDirty = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// If we had to re-assign orders, sort again just in case (though should be sorted)
|
|
||||||
if(orderIsDirty) {
|
|
||||||
newFields.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
setDisplayedFields(newFields);
|
|
||||||
setOrderChanged(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</Panel.Table>
|
|
||||||
</Panel.Section>
|
|
||||||
</Panel.Card>
|
|
||||||
</Panel.Section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
|
||||||
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
|
||||||
|
|
||||||
import SourcesPanel from './sources-panel/SourcesPanel';
|
|
||||||
import CustomFieldSettings from './CustomFields';
|
|
||||||
import ManageRundowns from './ManageRundowns';
|
|
||||||
import RundownDefaultSettings from './RundownDefaultSettings';
|
|
||||||
|
|
||||||
export default function ManagePanel({ location }: PanelBaseProps) {
|
|
||||||
const defaultsRef = useScrollIntoView<HTMLDivElement>('defaults', location);
|
|
||||||
const customRef = useScrollIntoView<HTMLDivElement>('custom', location);
|
|
||||||
const rundownsRef = useScrollIntoView<HTMLDivElement>('rundowns', location);
|
|
||||||
const sheetsRef = useScrollIntoView<HTMLDivElement>('sheets', location);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Panel.Header>Project data</Panel.Header>
|
|
||||||
<div ref={defaultsRef}>
|
|
||||||
<RundownDefaultSettings />
|
|
||||||
</div>
|
|
||||||
<div ref={customRef}>
|
|
||||||
<CustomFieldSettings />
|
|
||||||
</div>
|
|
||||||
<div ref={rundownsRef}>
|
|
||||||
<ManageRundowns />
|
|
||||||
</div>
|
|
||||||
<div ref={sheetsRef}>
|
|
||||||
<SourcesPanel />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import { IoAdd } from 'react-icons/io5';
|
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
|
||||||
|
|
||||||
import Button from '../../../../common/components/buttons/Button';
|
|
||||||
import Dialog from '../../../../common/components/dialog/Dialog';
|
|
||||||
import { useProjectRundowns } from '../../../../common/hooks-query/useProjectRundowns';
|
|
||||||
import { cx } from '../../../../common/utils/styleUtils';
|
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
|
||||||
|
|
||||||
import style from './ManagePanel.module.scss';
|
|
||||||
|
|
||||||
export default function ManageRundowns() {
|
|
||||||
const { data } = useProjectRundowns();
|
|
||||||
const [deleteOpen, deleteHandlers] = useDisclosure();
|
|
||||||
const [loadOpen, loadHandlers] = useDisclosure();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Panel.Section>
|
|
||||||
<Panel.Card>
|
|
||||||
<Panel.SubHeader>
|
|
||||||
Manage project rundowns
|
|
||||||
<Panel.InlineElements>
|
|
||||||
<Button onClick={() => undefined} disabled>
|
|
||||||
New <IoAdd />
|
|
||||||
</Button>
|
|
||||||
</Panel.InlineElements>
|
|
||||||
</Panel.SubHeader>
|
|
||||||
<Panel.Divider />
|
|
||||||
<Panel.Table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th># Entries</th>
|
|
||||||
<th style={{ width: '100%' }}>Title</th>
|
|
||||||
<th />
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{data.rundowns.map((rundown) => {
|
|
||||||
const isLoaded = data.loaded === rundown.id;
|
|
||||||
return (
|
|
||||||
<tr key={rundown.id} className={cx([isLoaded && style.current])}>
|
|
||||||
<td>{rundown.numEntries}</td>
|
|
||||||
<td>{`${rundown.title}${isLoaded && ' (loaded)'}`}</td>
|
|
||||||
<Panel.InlineElements as='td'>
|
|
||||||
<Button size='small' onClick={() => loadHandlers.open()} disabled={isLoaded}>
|
|
||||||
Load
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size='small'
|
|
||||||
variant='subtle-destructive'
|
|
||||||
onClick={() => deleteHandlers.open()}
|
|
||||||
disabled={isLoaded}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</Panel.InlineElements>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</Panel.Table>
|
|
||||||
</Panel.Card>
|
|
||||||
</Panel.Section>
|
|
||||||
<Dialog
|
|
||||||
isOpen={deleteOpen}
|
|
||||||
onClose={deleteHandlers.close}
|
|
||||||
title='Load rundown'
|
|
||||||
showBackdrop
|
|
||||||
showCloseButton
|
|
||||||
bodyElements={
|
|
||||||
<>
|
|
||||||
You will lose all data in your rundown. <br /> Are you sure?
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
footerElements={
|
|
||||||
<>
|
|
||||||
<Button size='large' onClick={deleteHandlers.close}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button variant='destructive' size='large' onClick={() => undefined}>
|
|
||||||
Delete rundown
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Dialog
|
|
||||||
isOpen={loadOpen}
|
|
||||||
onClose={loadHandlers.close}
|
|
||||||
title='Delete rundown'
|
|
||||||
showBackdrop
|
|
||||||
showCloseButton
|
|
||||||
bodyElements={
|
|
||||||
<>
|
|
||||||
The current playback will be stopped. <br /> Are you sure?
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
footerElements={
|
|
||||||
<>
|
|
||||||
<Button size='large' onClick={loadHandlers.close}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button variant='primary' size='large' onClick={() => undefined}>
|
|
||||||
Load rundown
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
-5
@@ -12,8 +12,3 @@
|
|||||||
color: $label-gray;
|
color: $label-gray;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copiableLink {
|
|
||||||
user-select: text;
|
|
||||||
color: $ui-white;
|
|
||||||
}
|
|
||||||
+53
-47
@@ -1,13 +1,13 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import QRCode from 'react-qr-code';
|
import QRCode from 'react-qr-code';
|
||||||
|
import { Button, Select, Switch } from '@chakra-ui/react';
|
||||||
|
|
||||||
import { generateUrl } from '../../../../common/api/session';
|
import { generateUrl } from '../../../../common/api/session';
|
||||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import Button from '../../../../common/components/buttons/Button';
|
|
||||||
import Info from '../../../../common/components/info/Info';
|
import Info from '../../../../common/components/info/Info';
|
||||||
import Select from '../../../../common/components/select/Select';
|
import useInfo from '../../../../common/hooks-query/useInfo';
|
||||||
import Switch from '../../../../common/components/switch/Switch';
|
import useUrlPresets from '../../../../common/hooks-query/useUrlPresets';
|
||||||
import copyToClipboard from '../../../../common/utils/copyToClipboard';
|
import copyToClipboard from '../../../../common/utils/copyToClipboard';
|
||||||
import { preventEscape } from '../../../../common/utils/keyEvent';
|
import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||||
import { linkToOtherHost } from '../../../../common/utils/linkUtils';
|
import { linkToOtherHost } from '../../../../common/utils/linkUtils';
|
||||||
@@ -16,12 +16,6 @@ import * as Panel from '../../panel-utils/PanelUtils';
|
|||||||
|
|
||||||
import style from './GenerateLinkForm.module.scss';
|
import style from './GenerateLinkForm.module.scss';
|
||||||
|
|
||||||
interface GenerateLinkFormProps {
|
|
||||||
hostOptions: { value: string; label: string }[];
|
|
||||||
pathOptions: { value: string; label: string }[];
|
|
||||||
isLockedToView?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GenerateLinkFormOptions {
|
interface GenerateLinkFormOptions {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
path: string;
|
path: string;
|
||||||
@@ -31,21 +25,22 @@ interface GenerateLinkFormOptions {
|
|||||||
|
|
||||||
type GenerateLinkState = 'pending' | 'loading' | 'success' | 'error';
|
type GenerateLinkState = 'pending' | 'loading' | 'success' | 'error';
|
||||||
|
|
||||||
export default function GenerateLinkForm({ hostOptions, pathOptions, isLockedToView }: GenerateLinkFormProps) {
|
export default function GenerateLinkForm() {
|
||||||
|
const { data: infoData } = useInfo();
|
||||||
|
const { data: urlPresetData } = useUrlPresets();
|
||||||
const [formState, setFormState] = useState<GenerateLinkState>('pending');
|
const [formState, setFormState] = useState<GenerateLinkState>('pending');
|
||||||
const [url, setUrl] = useState(serverURL);
|
const [url, setUrl] = useState(serverURL);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
register,
|
||||||
setError,
|
setError,
|
||||||
watch,
|
|
||||||
setValue,
|
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<GenerateLinkFormOptions>({
|
} = useForm<GenerateLinkFormOptions>({
|
||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
baseUrl: currentHostName,
|
baseUrl: currentHostName,
|
||||||
path: isLockedToView ? pathOptions[0].value : 'timer',
|
path: '',
|
||||||
lock: false,
|
lock: false,
|
||||||
authenticate: false,
|
authenticate: false,
|
||||||
},
|
},
|
||||||
@@ -75,64 +70,75 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, isLockedToV
|
|||||||
return (
|
return (
|
||||||
<Panel.Section as='form' onSubmit={handleSubmit(onSubmit)} onKeyDown={(event) => preventEscape(event)}>
|
<Panel.Section as='form' onSubmit={handleSubmit(onSubmit)} onKeyDown={(event) => preventEscape(event)}>
|
||||||
{errors.root && <Panel.Error>{errors.root.message}</Panel.Error>}
|
{errors.root && <Panel.Error>{errors.root.message}</Panel.Error>}
|
||||||
{!isLockedToView ? (
|
<Info>
|
||||||
<Info>
|
<Panel.Paragraph>
|
||||||
<Panel.Paragraph>
|
You can generate a link to share with your team or to use in automation (such as companion).
|
||||||
You can generate a link to share with your team or to use in automation (such as companion).
|
</Panel.Paragraph>
|
||||||
</Panel.Paragraph>
|
</Info>
|
||||||
</Info>
|
|
||||||
) : (
|
|
||||||
<Info>
|
|
||||||
<Panel.Paragraph>You can generate a link to share with your team</Panel.Paragraph>
|
|
||||||
</Info>
|
|
||||||
)}
|
|
||||||
<Panel.ListGroup>
|
<Panel.ListGroup>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field
|
<Panel.Field
|
||||||
title='Host IP'
|
title='Host IP'
|
||||||
description={`Which IP address will be used${isOntimeCloud ? ' (not applicable in Ontime Cloud)' : ''}`}
|
description={`Which IP address will be used${isOntimeCloud ? ' (not applicable in Ontime Cloud)' : ''}`}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select variant='ontime' isDisabled={isOntimeCloud} size='sm' {...register('baseUrl')}>
|
||||||
disabled={isOntimeCloud}
|
{infoData.networkInterfaces.map((nif) => {
|
||||||
options={hostOptions}
|
return (
|
||||||
value={watch('baseUrl')}
|
<option key={nif.name} value={nif.address}>
|
||||||
onValueChange={(value) => setValue('baseUrl', value)}
|
{`${nif.name} - ${nif.address}`}
|
||||||
/>
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</Panel.ListItem>
|
||||||
|
<Panel.ListItem>
|
||||||
|
<Panel.Field
|
||||||
|
title='URL Preset'
|
||||||
|
description='Which preset will the link point to (will default to /timer if none is given)'
|
||||||
|
/>
|
||||||
|
<Select variant='ontime' size='sm' {...register('path')}>
|
||||||
|
<option key='timer' value='timer'>
|
||||||
|
Timer
|
||||||
|
</option>
|
||||||
|
<option key='companion' value=''>
|
||||||
|
Companion
|
||||||
|
</option>
|
||||||
|
{urlPresetData.map((preset) => {
|
||||||
|
return (
|
||||||
|
<option key={preset.alias} value={preset.alias}>
|
||||||
|
{`Preset: ${preset.alias}`}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
{isLockedToView ? (
|
|
||||||
<input type='hidden' value={watch('path')} />
|
|
||||||
) : (
|
|
||||||
<Panel.ListItem>
|
|
||||||
<Panel.Field title='Ontime view' description='Which view or preset will the link point to' />
|
|
||||||
<Select options={pathOptions} value={watch('path')} onValueChange={(value) => setValue('path', value)} />
|
|
||||||
</Panel.ListItem>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field
|
<Panel.Field
|
||||||
title='Lock navigation'
|
title='Lock navigation'
|
||||||
description='Prevent showing navigation (will only work for non production URLs)'
|
description='Prevent showing navigation (will only work for non production URLs)'
|
||||||
/>
|
/>
|
||||||
<Switch name='lock' checked={watch('lock')} onCheckedChange={(checked) => setValue('lock', checked)} />
|
<Switch variant='ontime' size='lg' {...register('lock')} />
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field title='Authenticate' description='Whether the URL should be pre-authenticated' />
|
<Panel.Field title='Authenticate' description='Whether the URL should be pre-authenticated' />
|
||||||
<Switch
|
<Switch variant='ontime' size='lg' {...register('authenticate')} />
|
||||||
name='authenticate'
|
|
||||||
checked={watch('authenticate')}
|
|
||||||
onCheckedChange={(checked) => setValue('authenticate', checked)}
|
|
||||||
/>
|
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
</Panel.ListGroup>
|
</Panel.ListGroup>
|
||||||
<Panel.ListGroup>
|
<Panel.ListGroup>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field title='Generate link' description='Fill form and generate link and QR code' />
|
<Panel.Field title='Generate link' description='Fill form and generate link and QR code' />
|
||||||
<Button variant='primary' loading={formState === 'loading'} type='submit' style={{ alignSelf: 'end' }}>
|
<Button
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
isLoading={formState === 'loading'}
|
||||||
|
type='submit'
|
||||||
|
style={{ alignSelf: 'end' }}
|
||||||
|
>
|
||||||
{formState === 'success' ? 'Link copied to clipboard!' : 'Update share link'}
|
{formState === 'success' ? 'Link copied to clipboard!' : 'Update share link'}
|
||||||
</Button>
|
</Button>
|
||||||
<div className={style.column}>
|
<div className={style.column}>
|
||||||
<QRCode size={172} value={url} className={style.qrCode} />
|
<QRCode size={172} value={url} className={style.qrCode} />
|
||||||
<div className={style.copiableLink}>{url}</div>
|
<div>{url}</div>
|
||||||
</div>
|
</div>
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
</Panel.ListGroup>
|
</Panel.ListGroup>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { MouseEvent } from 'react';
|
import { MouseEvent } from 'react';
|
||||||
import { IoArrowUp } from 'react-icons/io5';
|
import { IoArrowUp } from 'react-icons/io5';
|
||||||
|
import { Button } from '@chakra-ui/react';
|
||||||
|
|
||||||
import Button from '../../../../common/components/buttons/Button';
|
|
||||||
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 '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
@@ -18,8 +18,13 @@ export default function LogExport() {
|
|||||||
<Panel.Card>
|
<Panel.Card>
|
||||||
<Panel.SubHeader>
|
<Panel.SubHeader>
|
||||||
Event log
|
Event log
|
||||||
<Button onClick={extract}>
|
<Button
|
||||||
Extract <IoArrowUp className={style.iconRotate} />
|
variant='ontime-subtle'
|
||||||
|
size='sm'
|
||||||
|
rightIcon={<IoArrowUp className={style.iconRotate} />}
|
||||||
|
onClick={extract}
|
||||||
|
>
|
||||||
|
Extract
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.SubHeader>
|
</Panel.SubHeader>
|
||||||
<Panel.Divider />
|
<Panel.Divider />
|
||||||
|
|||||||
@@ -4,14 +4,17 @@ import { MessageTag } from 'ontime-types';
|
|||||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||||
import { usePing } from '../../../../common/hooks/useSocket';
|
import { usePing } from '../../../../common/hooks/useSocket';
|
||||||
import { sendSocket } from '../../../../common/utils/socket';
|
import { sendSocket } from '../../../../common/utils/socket';
|
||||||
import { isDockerImage } from '../../../../externals';
|
import { isDockerImage, isOntimeCloud } from '../../../../externals';
|
||||||
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
import ClientControlPanel from '../client-control-panel/ClientControlPanel';
|
||||||
|
|
||||||
import ClientControlPanel from './client-control/ClientControlPanel';
|
import GenerateLinkForm from './GenerateLinkForm';
|
||||||
|
import InfoNif from './NetworkInterfaces';
|
||||||
import LogExport from './NetworkLogExport';
|
import LogExport from './NetworkLogExport';
|
||||||
|
|
||||||
export default function NetworkLogPanel({ location }: PanelBaseProps) {
|
export default function NetworkLogPanel({ location }: PanelBaseProps) {
|
||||||
|
const linkRef = useScrollIntoView<HTMLDivElement>('link', location);
|
||||||
const clientsRef = useScrollIntoView<HTMLDivElement>('clients', location);
|
const clientsRef = useScrollIntoView<HTMLDivElement>('clients', location);
|
||||||
const logRef = useScrollIntoView<HTMLDivElement>('log', location);
|
const logRef = useScrollIntoView<HTMLDivElement>('log', location);
|
||||||
|
|
||||||
@@ -23,6 +26,21 @@ export default function NetworkLogPanel({ location }: PanelBaseProps) {
|
|||||||
<OntimeCloudStats />
|
<OntimeCloudStats />
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
)}
|
)}
|
||||||
|
<div ref={linkRef}>
|
||||||
|
<Panel.Section>
|
||||||
|
<Panel.Card>
|
||||||
|
<Panel.SubHeader>Share Ontime Link</Panel.SubHeader>
|
||||||
|
<Panel.Divider />
|
||||||
|
{!isOntimeCloud && (
|
||||||
|
<>
|
||||||
|
<Panel.Paragraph>Ontime is streaming on the following network interfaces</Panel.Paragraph>
|
||||||
|
<InfoNif />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<GenerateLinkForm />
|
||||||
|
</Panel.Card>
|
||||||
|
</Panel.Section>
|
||||||
|
</div>
|
||||||
<div ref={logRef}>
|
<div ref={logRef}>
|
||||||
<LogExport />
|
<LogExport />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { ChangeEvent, useRef, useState } from 'react';
|
import { ChangeEvent, useRef, useState } from 'react';
|
||||||
import { IoAdd } from 'react-icons/io5';
|
import { IoAdd } from 'react-icons/io5';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import { Button, Input } from '@chakra-ui/react';
|
||||||
|
|
||||||
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 Button from '../../../../common/components/buttons/Button';
|
|
||||||
import { validateProjectFile } from '../../../../common/utils/uploadUtils';
|
import { validateProjectFile } from '../../../../common/utils/uploadUtils';
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ export default function ManageProjects() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel.Section>
|
<Panel.Section>
|
||||||
<input
|
<Input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
type='file'
|
type='file'
|
||||||
@@ -70,14 +70,22 @@ export default function ManageProjects() {
|
|||||||
Manage projects
|
Manage projects
|
||||||
<Panel.InlineElements>
|
<Panel.InlineElements>
|
||||||
<Button
|
<Button
|
||||||
|
variant='ontime-subtle'
|
||||||
onClick={handleSelectFile}
|
onClick={handleSelectFile}
|
||||||
disabled={Boolean(loading) || isCreatingProject}
|
size='sm'
|
||||||
loading={loading === 'import'}
|
isDisabled={Boolean(loading) || isCreatingProject}
|
||||||
|
isLoading={loading === 'import'}
|
||||||
>
|
>
|
||||||
Import
|
Import
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleToggleCreate} disabled={Boolean(loading) || isCreatingProject}>
|
<Button
|
||||||
New <IoAdd />
|
variant='ontime-subtle'
|
||||||
|
onClick={handleToggleCreate}
|
||||||
|
size='sm'
|
||||||
|
isDisabled={Boolean(loading) || isCreatingProject}
|
||||||
|
rightIcon={<IoAdd />}
|
||||||
|
>
|
||||||
|
New
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
</Panel.SubHeader>
|
</Panel.SubHeader>
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useFieldArray, useForm } from 'react-hook-form';
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
import { IoAdd, IoTrash } from 'react-icons/io5';
|
import { IoTrash } from 'react-icons/io5';
|
||||||
|
import { Button, Input, Textarea } from '@chakra-ui/react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
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 Button from '../../../../common/components/buttons/Button';
|
|
||||||
import Input from '../../../../common/components/input/input/Input';
|
|
||||||
import Textarea from '../../../../common/components/input/textarea/Textarea';
|
|
||||||
import { preventEscape } from '../../../../common/utils/keyEvent';
|
import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||||
import { documentationUrl } from '../../../../externals';
|
import { documentationUrl } from '../../../../externals';
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
@@ -88,10 +86,10 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
|||||||
<Panel.Title>
|
<Panel.Title>
|
||||||
Create new project
|
Create new project
|
||||||
<Panel.InlineElements>
|
<Panel.InlineElements>
|
||||||
<Button onClick={onClose} variant='ghosted' disabled={isSubmitting}>
|
<Button onClick={onClose} variant='ontime-ghosted' size='sm' isDisabled={isSubmitting}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button disabled={!isValid} type='submit' loading={isSubmitting} variant='primary'>
|
<Button isDisabled={!isValid} type='submit' isLoading={isSubmitting} variant='ontime-filled' size='sm'>
|
||||||
Create project
|
Create project
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
@@ -100,31 +98,53 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
|||||||
<Panel.Section className={style.innerColumn}>
|
<Panel.Section className={style.innerColumn}>
|
||||||
<label>
|
<label>
|
||||||
Project title
|
Project title
|
||||||
<Input fluid maxLength={50} placeholder='Your project name' {...register('title')} />
|
<Input
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
maxLength={50}
|
||||||
|
placeholder='Your project name'
|
||||||
|
autoComplete='off'
|
||||||
|
{...register('title')}
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Project description
|
Project description
|
||||||
<Input fluid maxLength={100} placeholder='Euro Love, Malmö 2024' {...register('description')} />
|
<Input
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
maxLength={100}
|
||||||
|
placeholder='Euro Love, Malmö 2024'
|
||||||
|
autoComplete='off'
|
||||||
|
{...register('description')}
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Backstage info
|
Backstage info
|
||||||
<Textarea
|
<Textarea
|
||||||
fluid
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
maxLength={150}
|
maxLength={150}
|
||||||
placeholder='Wi-Fi password: 1234'
|
placeholder='Wi-Fi password: 1234'
|
||||||
resize='vertical'
|
autoComplete='off'
|
||||||
|
resize='none'
|
||||||
{...register('backstageInfo')}
|
{...register('backstageInfo')}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Backstage QR code Url
|
Backstage QR code Url
|
||||||
<Input fluid placeholder={documentationUrl} {...register('backstageUrl')} />
|
<Input
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
placeholder={documentationUrl}
|
||||||
|
autoComplete='off'
|
||||||
|
{...register('backstageUrl')}
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<Panel.Section>
|
<Panel.Section>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field title='Custom data' description='Add custom data for your project' />
|
<Panel.Field title='Custom data' description='Add custom data for your project' />
|
||||||
<Button onClick={handleAddCustom}>
|
<Button variant='ontime-subtle' onClick={handleAddCustom}>
|
||||||
Add <IoAdd />
|
+
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
{fields.map((field, idx) => (
|
{fields.map((field, idx) => (
|
||||||
@@ -132,13 +152,25 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
|||||||
<Panel.Paragraph>{idx + 1}.</Panel.Paragraph>
|
<Panel.Paragraph>{idx + 1}.</Panel.Paragraph>
|
||||||
<label>
|
<label>
|
||||||
Title
|
Title
|
||||||
<Input placeholder={field.title} {...register(`custom.${idx}.title` as const)} />
|
<Input
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
placeholder={field.title}
|
||||||
|
autoComplete='off'
|
||||||
|
{...register(`custom.${idx}.title` as const)}
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Value
|
Value
|
||||||
<Input placeholder={field.value} autoComplete='off' {...register(`custom.${idx}.value` as const)} />
|
<Input
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
placeholder={field.value}
|
||||||
|
autoComplete='off'
|
||||||
|
{...register(`custom.${idx}.value` as const)}
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<Button variant='ghosted' onClick={() => remove(idx)}>
|
<Button variant='ontime-ghosted' onClick={() => remove(idx)}>
|
||||||
<IoTrash />
|
<IoTrash />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+80
-40
@@ -1,21 +1,19 @@
|
|||||||
import { ChangeEvent, useEffect, useRef } from 'react';
|
import { ChangeEvent, useEffect, useRef } from 'react';
|
||||||
import { useFieldArray, useForm } from 'react-hook-form';
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
import { IoAdd, IoDownloadOutline, IoTrash } from 'react-icons/io5';
|
import { IoAdd, IoDownloadOutline, IoTrash } from 'react-icons/io5';
|
||||||
|
import { Button, Input, Textarea } from '@chakra-ui/react';
|
||||||
import { type ProjectData } from 'ontime-types';
|
import { type ProjectData } from 'ontime-types';
|
||||||
|
|
||||||
import { projectLogoPath } from '../../../../common/api/constants';
|
import { projectLogoPath } from '../../../../common/api/constants';
|
||||||
import { postProjectData, uploadProjectLogo } from '../../../../common/api/project';
|
import { postProjectData, uploadProjectLogo } from '../../../../common/api/project';
|
||||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import Button from '../../../../common/components/buttons/Button';
|
|
||||||
import Input from '../../../../common/components/input/input/Input';
|
|
||||||
import Textarea from '../../../../common/components/input/textarea/Textarea';
|
|
||||||
import useProjectData from '../../../../common/hooks-query/useProjectData';
|
import useProjectData from '../../../../common/hooks-query/useProjectData';
|
||||||
import { preventEscape } from '../../../../common/utils/keyEvent';
|
import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||||
import { validateLogo } from '../../../../common/utils/uploadUtils';
|
import { validateLogo } from '../../../../common/utils/uploadUtils';
|
||||||
import { documentationUrl } from '../../../../externals';
|
import { documentationUrl } from '../../../../externals';
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import style from './SettingsPanel.module.scss';
|
import style from './ProjectPanel.module.scss';
|
||||||
|
|
||||||
export default function ProjectData() {
|
export default function ProjectData() {
|
||||||
const { data, status, refetch } = useProjectData();
|
const { data, status, refetch } = useProjectData();
|
||||||
@@ -114,10 +112,16 @@ export default function ProjectData() {
|
|||||||
<Panel.SubHeader>
|
<Panel.SubHeader>
|
||||||
Project data
|
Project data
|
||||||
<Panel.InlineElements>
|
<Panel.InlineElements>
|
||||||
<Button onClick={onReset} disabled={isSubmitting || !isDirty}>
|
<Button variant='ontime-ghosted' size='sm' onClick={onReset} isDisabled={isSubmitting || !isDirty}>
|
||||||
Revert to saved
|
Revert to saved
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant='primary' type='submit' disabled={!isDirty || !isValid} loading={isSubmitting}>
|
<Button
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
type='submit'
|
||||||
|
isDisabled={!isDirty || !isValid}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
@@ -128,16 +132,20 @@ export default function ProjectData() {
|
|||||||
<label>
|
<label>
|
||||||
Project title
|
Project title
|
||||||
<Input
|
<Input
|
||||||
fluid
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
maxLength={50}
|
maxLength={50}
|
||||||
placeholder='Project title is shown in production views'
|
placeholder='Project title is shown in production views'
|
||||||
|
autoComplete='off'
|
||||||
{...register('title')}
|
{...register('title')}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<Panel.Section style={{ marginTop: 0 }}>
|
<Panel.Section style={{ marginTop: 0 }}>
|
||||||
<label>
|
<label>
|
||||||
Project logo
|
Project logo
|
||||||
<input
|
<Input
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
type='file'
|
type='file'
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
accept='image/*'
|
accept='image/*'
|
||||||
@@ -153,17 +161,25 @@ export default function ProjectData() {
|
|||||||
<>
|
<>
|
||||||
<img src={`${projectLogoPath}/${watch('projectLogo')}`} />
|
<img src={`${projectLogoPath}/${watch('projectLogo')}`} />
|
||||||
<Button
|
<Button
|
||||||
variant='subtle-destructive'
|
size='sm'
|
||||||
disabled={isSubmitting || !watch('projectLogo')}
|
variant='ontime-filled'
|
||||||
|
isDisabled={isSubmitting || !watch('projectLogo')}
|
||||||
|
leftIcon={<IoTrash />}
|
||||||
onClick={handleDeleteLogo}
|
onClick={handleDeleteLogo}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
<IoTrash />
|
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Button disabled={isSubmitting} onClick={handleClickUpload} type='button'>
|
<Button
|
||||||
<IoDownloadOutline />
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
leftIcon={<IoDownloadOutline />}
|
||||||
|
onClick={handleClickUpload}
|
||||||
|
type='button'
|
||||||
|
>
|
||||||
Upload logo
|
Upload logo
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -171,30 +187,44 @@ export default function ProjectData() {
|
|||||||
</Panel.Card>
|
</Panel.Card>
|
||||||
</label>
|
</label>
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
Project description
|
Project description
|
||||||
<Input fluid maxLength={100} placeholder='Euro Love, Malmö 2024' {...register('description')} />
|
<Input
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
maxLength={100}
|
||||||
|
placeholder='Euro Love, Malmö 2024'
|
||||||
|
autoComplete='off'
|
||||||
|
{...register('description')}
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Backstage info
|
Backstage info
|
||||||
<Textarea
|
<Textarea
|
||||||
fluid
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
maxLength={150}
|
maxLength={150}
|
||||||
placeholder='Wi-Fi password: 1234'
|
placeholder='Wi-Fi password: 1234'
|
||||||
resize='vertical'
|
autoComplete='off'
|
||||||
|
resize='none'
|
||||||
{...register('backstageInfo')}
|
{...register('backstageInfo')}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Backstage QR code URL
|
Backstage QR code URL
|
||||||
<Input fluid placeholder={documentationUrl} {...register('backstageUrl')} />
|
<Input
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
placeholder={documentationUrl}
|
||||||
|
autoComplete='off'
|
||||||
|
{...register('backstageUrl')}
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<Panel.Section style={{ marginTop: 0 }}>
|
<Panel.Section style={{ marginTop: 0 }}>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field title='Custom data' description='' />
|
<Panel.Field title='Custom data' description='' />
|
||||||
<Button onClick={handleAddCustom}>
|
<Button leftIcon={<IoAdd />} size='sm' variant='ontime-subtle' onClick={handleAddCustom}>
|
||||||
Add <IoAdd />
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
{fields.length > 0 &&
|
{fields.length > 0 &&
|
||||||
@@ -207,31 +237,41 @@ export default function ProjectData() {
|
|||||||
| undefined;
|
| undefined;
|
||||||
return (
|
return (
|
||||||
<div key={field.id} className={style.customDataItem}>
|
<div key={field.id} className={style.customDataItem}>
|
||||||
<div className={style.titleRow}>
|
<div>
|
||||||
<label>
|
<div className={style.titleRow}>
|
||||||
Title
|
<label>
|
||||||
<Input
|
Title
|
||||||
fluid
|
<Input
|
||||||
defaultValue={field.title}
|
variant='ontime-filled'
|
||||||
placeholder='Title of your custom data'
|
size='sm'
|
||||||
{...register(`custom.${idx}.title`, {
|
defaultValue={field.title}
|
||||||
required: { value: true, message: 'Field cannot be empty' },
|
placeholder='Title of your custom data'
|
||||||
})}
|
autoComplete='off'
|
||||||
/>
|
{...register(`custom.${idx}.title`, {
|
||||||
</label>
|
required: { value: true, message: 'Field cannot be empty' },
|
||||||
<Button variant='subtle-destructive' onClick={() => remove(idx)}>
|
})}
|
||||||
<IoTrash />
|
/>
|
||||||
Delete Entry
|
</label>
|
||||||
</Button>
|
<Button
|
||||||
|
size='sm'
|
||||||
|
variant='ontime-subtle'
|
||||||
|
color='#FA5656' // $red-500
|
||||||
|
onClick={() => remove(idx)}
|
||||||
|
leftIcon={<IoTrash />}
|
||||||
|
>
|
||||||
|
Delete Entry
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{rowErrors?.title?.message && <Panel.Error>{rowErrors.title.message}</Panel.Error>}
|
||||||
</div>
|
</div>
|
||||||
{rowErrors?.title?.message && <Panel.Error>{rowErrors.title.message}</Panel.Error>}
|
|
||||||
<label>
|
<label>
|
||||||
Value
|
Value
|
||||||
<Textarea
|
<Textarea
|
||||||
fluid
|
variant='ontime-filled'
|
||||||
rows={3}
|
resize='none'
|
||||||
resize='vertical'
|
size='sm'
|
||||||
defaultValue={field.value}
|
defaultValue={field.value}
|
||||||
|
autoComplete='off'
|
||||||
placeholder='Text of your custom data'
|
placeholder='Text of your custom data'
|
||||||
{...register(`custom.${idx}.value`, {
|
{...register(`custom.${idx}.value`, {
|
||||||
required: { value: true, message: 'Field cannot be empty' },
|
required: { value: true, message: 'Field cannot be empty' },
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { Button, Input } from '@chakra-ui/react';
|
||||||
|
|
||||||
import Button from '../../../../common/components/buttons/Button';
|
|
||||||
import Input from '../../../../common/components/input/input/Input';
|
|
||||||
import { preventEscape } from '../../../../common/utils/keyEvent';
|
import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
@@ -46,16 +45,21 @@ export default function ProjectForm({ action, filename, onSubmit, onCancel }: Pr
|
|||||||
<Input
|
<Input
|
||||||
className={style.formInput}
|
className={style.formInput}
|
||||||
id='filename'
|
id='filename'
|
||||||
|
size='sm'
|
||||||
|
type='text'
|
||||||
|
variant='ontime-filled'
|
||||||
placeholder='Enter new name'
|
placeholder='Enter new name'
|
||||||
|
autoComplete='off'
|
||||||
{...register('filename', { required: true })}
|
{...register('filename', { required: true })}
|
||||||
/>
|
/>
|
||||||
<Panel.InlineElements relation='inner'>
|
<Panel.InlineElements relation='inner'>
|
||||||
<Button onClick={onCancel} variant='ghosted' disabled={isSubmitting}>
|
<Button onClick={onCancel} size='sm' variant='ontime-ghosted' disabled={isSubmitting}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant='primary'
|
size='sm'
|
||||||
disabled={!isDirty || !isValid || isSubmitting}
|
variant='ontime-filled'
|
||||||
|
isDisabled={!isDirty || !isValid || isSubmitting}
|
||||||
type='submit'
|
type='submit'
|
||||||
className={style.saveButton}
|
className={style.saveButton}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import Info from '../../../../common/components/info/Info';
|
|
||||||
import { useOrderedProjectList } from '../../../../common/hooks-query/useProjectList';
|
import { useOrderedProjectList } from '../../../../common/hooks-query/useProjectList';
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
@@ -9,7 +8,7 @@ import ProjectListItem, { EditMode } from './ProjectListItem';
|
|||||||
import style from './ProjectPanel.module.scss';
|
import style from './ProjectPanel.module.scss';
|
||||||
|
|
||||||
export default function ProjectList() {
|
export default function ProjectList() {
|
||||||
const { data, refetch, status } = useOrderedProjectList();
|
const { data, refetch } = useOrderedProjectList();
|
||||||
|
|
||||||
const [editingMode, setEditingMode] = useState<EditMode | null>(null);
|
const [editingMode, setEditingMode] = useState<EditMode | null>(null);
|
||||||
const [editingFilename, setEditingFilename] = useState<string | null>(null);
|
const [editingFilename, setEditingFilename] = useState<string | null>(null);
|
||||||
@@ -28,47 +27,30 @@ export default function ProjectList() {
|
|||||||
await refetch();
|
await refetch();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (status === 'pending') {
|
|
||||||
return (
|
|
||||||
<div className={style.empty}>
|
|
||||||
<Panel.Loader isLoading />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const numProjects = data.reorderedProjectFiles.length;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Panel.Table>
|
||||||
{numProjects > 20 && (
|
<thead>
|
||||||
<Info className={style.warningInfo} type='warning'>
|
<tr>
|
||||||
You have {numProjects} projects. Consider deleting unused projects to improve performance.
|
<th className={style.containCell}>File Name</th>
|
||||||
</Info>
|
<th>Last Used</th>
|
||||||
)}
|
<th />
|
||||||
<Panel.Table>
|
</tr>
|
||||||
<thead>
|
</thead>
|
||||||
<tr>
|
<tbody>
|
||||||
<th className={style.containCell}>File Name</th>
|
{data.reorderedProjectFiles.map((project) => (
|
||||||
<th>Last Used</th>
|
<ProjectListItem
|
||||||
<th />
|
key={project.filename}
|
||||||
</tr>
|
filename={project.filename}
|
||||||
</thead>
|
updatedAt={project.updatedAt}
|
||||||
<tbody>
|
onToggleEditMode={handleToggleEditMode}
|
||||||
{data.reorderedProjectFiles.map((project) => (
|
onSubmit={handleClear}
|
||||||
<ProjectListItem
|
onRefetch={handleRefetch}
|
||||||
key={project.filename}
|
editingFilename={editingFilename}
|
||||||
filename={project.filename}
|
editingMode={editingMode}
|
||||||
updatedAt={project.updatedAt}
|
current={project.filename === data.lastLoadedProject}
|
||||||
onToggleEditMode={handleToggleEditMode}
|
/>
|
||||||
onSubmit={handleClear}
|
))}
|
||||||
onRefetch={handleRefetch}
|
</tbody>
|
||||||
editingFilename={editingFilename}
|
</Panel.Table>
|
||||||
editingMode={editingMode}
|
|
||||||
current={project.filename === data.lastLoadedProject}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</Panel.Table>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { Switch } from '@chakra-ui/react';
|
import { Button, Switch } from '@chakra-ui/react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { PROJECT_DATA } from '../../../../common/api/constants';
|
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 Button from '../../../../common/components/buttons/Button';
|
|
||||||
import { cx } from '../../../../common/utils/styleUtils';
|
import { cx } from '../../../../common/utils/styleUtils';
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
@@ -77,10 +76,16 @@ export default function ProjectMergeForm(props: ProjectMergeFromProps) {
|
|||||||
<Panel.Title>
|
<Panel.Title>
|
||||||
Merge {`"${fileName}"`}
|
Merge {`"${fileName}"`}
|
||||||
<Panel.InlineElements>
|
<Panel.InlineElements>
|
||||||
<Button onClick={onClose} variant='ghosted' disabled={isSubmitting}>
|
<Button onClick={onClose} variant='ontime-ghosted' size='sm' isDisabled={isSubmitting}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type='submit' disabled={!isValid || !isDirty} loading={isSubmitting} variant='primary'>
|
<Button
|
||||||
|
isDisabled={!isValid || !isDirty}
|
||||||
|
type='submit'
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
>
|
||||||
Merge
|
Merge
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
|
|||||||
+29
-10
@@ -26,10 +26,6 @@
|
|||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fullWidth {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.innerColumn {
|
.innerColumn {
|
||||||
margin: 0 2rem;
|
margin: 0 2rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
@@ -45,11 +41,34 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.warningInfo {
|
.uploadLogoCard {
|
||||||
margin-bottom: 1rem;
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
background-color: $gray-1350;
|
||||||
|
border: 1px solid $white-10;
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 250px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
.customDataItem {
|
||||||
height: 300px;
|
display: contents;
|
||||||
position: relative;
|
width: 100%;
|
||||||
}
|
|
||||||
|
.titleRow{
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: end;
|
||||||
|
|
||||||
|
label {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,13 +5,15 @@ import QuickStart from '../../quick-start/QuickStart';
|
|||||||
import type { SettingsOptionId } from '../../useAppSettingsMenu';
|
import type { SettingsOptionId } from '../../useAppSettingsMenu';
|
||||||
|
|
||||||
import ManageProjects from './ManageProjects';
|
import ManageProjects from './ManageProjects';
|
||||||
|
import ProjectData from './ProjectData';
|
||||||
|
|
||||||
interface ProjectPanelProps extends PanelBaseProps {
|
interface ProjectPanelProps extends PanelBaseProps {
|
||||||
setLocation: (location: SettingsOptionId) => void;
|
setLocation: (location: SettingsOptionId) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProjectPanel({ location, setLocation }: ProjectPanelProps) {
|
export default function ProjectPanel({ location, setLocation }: ProjectPanelProps) {
|
||||||
const manageProjectsRef = useScrollIntoView<HTMLDivElement>('list', location);
|
const projectRef = useScrollIntoView<HTMLDivElement>('data', location);
|
||||||
|
const manageRef = useScrollIntoView<HTMLDivElement>('manage', location);
|
||||||
|
|
||||||
const handleQuickClose = () => {
|
const handleQuickClose = () => {
|
||||||
setLocation('project');
|
setLocation('project');
|
||||||
@@ -21,7 +23,10 @@ export default function ProjectPanel({ location, setLocation }: ProjectPanelProp
|
|||||||
<>
|
<>
|
||||||
<Panel.Header>Project</Panel.Header>
|
<Panel.Header>Project</Panel.Header>
|
||||||
<QuickStart isOpen={location === 'create'} onClose={handleQuickClose} />
|
<QuickStart isOpen={location === 'create'} onClose={handleQuickClose} />
|
||||||
<div ref={manageProjectsRef}>
|
<div ref={projectRef}>
|
||||||
|
<ProjectData />
|
||||||
|
</div>
|
||||||
|
<div ref={manageRef}>
|
||||||
<ManageProjects />
|
<ManageProjects />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
.uploadLogoCard {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
background-color: $gray-1350;
|
|
||||||
border: 1px solid $white-10;
|
|
||||||
border-radius: 3px;
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 250px;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.customDataItem {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.titleRow {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
align-items: end;
|
|
||||||
|
|
||||||
label {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
|
||||||
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
|
||||||
|
|
||||||
import GeneralSettings from './GeneralSettings';
|
|
||||||
import ProjectData from './ProjectData';
|
|
||||||
import ViewSettings from './ViewSettings';
|
|
||||||
|
|
||||||
export default function SettingsPanel({ location }: PanelBaseProps) {
|
|
||||||
const dataRef = useScrollIntoView<HTMLDivElement>('data', location);
|
|
||||||
const generalRef = useScrollIntoView<HTMLDivElement>('general', location);
|
|
||||||
const viewRef = useScrollIntoView<HTMLDivElement>('view', location);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Panel.Header>Settings</Panel.Header>
|
|
||||||
<div ref={dataRef}>
|
|
||||||
<ProjectData />
|
|
||||||
</div>
|
|
||||||
<div ref={generalRef}>
|
|
||||||
<GeneralSettings />
|
|
||||||
</div>
|
|
||||||
<div ref={viewRef}>
|
|
||||||
<ViewSettings />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,27 @@
|
|||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogBody,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
Button,
|
||||||
|
useDisclosure,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
import Button from '../../../../common/components/buttons/Button';
|
|
||||||
import Dialog from '../../../../common/components/dialog/Dialog';
|
|
||||||
import { useElectronEvent } from '../../../../common/hooks/useElectronEvent';
|
import { useElectronEvent } from '../../../../common/hooks/useElectronEvent';
|
||||||
import { isLocalhost, isOntimeCloud } from '../../../../externals';
|
import { isLocalhost, isOntimeCloud } from '../../../../externals';
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
export default function ShutdownPanel() {
|
export default function ShutdownPanel() {
|
||||||
const { isElectron, sendToElectron } = useElectronEvent();
|
const { isElectron, sendToElectron } = useElectronEvent();
|
||||||
const [isOpen, handler] = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
const cancelRef = useRef<HTMLButtonElement | null>(null);
|
const cancelRef = useRef<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
const sendShutdown = () => {
|
const sendShutdown = () => {
|
||||||
sendToElectron('shutdown', 'now');
|
sendToElectron('shutdown', 'now');
|
||||||
handler.close();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const canShutdown = isElectron || isLocalhost;
|
const canShutdown = isElectron || isLocalhost;
|
||||||
@@ -33,33 +40,32 @@ export default function ShutdownPanel() {
|
|||||||
The runtime state will be lost, but your project is kept for next time.
|
The runtime state will be lost, but your project is kept for next time.
|
||||||
</Panel.Paragraph>
|
</Panel.Paragraph>
|
||||||
)}
|
)}
|
||||||
<Button variant='destructive' onClick={handler.open} disabled={!(isElectron || isLocalhost)}>
|
<Button colorScheme='red' onClick={onOpen} maxWidth='350px' isDisabled={!(isElectron || isLocalhost)}>
|
||||||
Shutdown ontime
|
Shutdown ontime
|
||||||
</Button>
|
</Button>
|
||||||
{!canShutdown && (
|
{!canShutdown && (
|
||||||
<Panel.Description>Note: Ontime can only be shutdown from the machine it is running in.</Panel.Description>
|
<Panel.Description>Note: Ontime can only be shutdown from the machine it is running in.</Panel.Description>
|
||||||
)}
|
)}
|
||||||
<Dialog
|
<AlertDialog variant='ontime' isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
|
||||||
isOpen={isOpen}
|
<AlertDialogOverlay>
|
||||||
title='Shutdown Ontime'
|
<AlertDialogContent>
|
||||||
showCloseButton
|
<AlertDialogHeader fontSize='lg' fontWeight='bold'>
|
||||||
onClose={handler.close}
|
Ontime Shutdown
|
||||||
bodyElements={
|
</AlertDialogHeader>
|
||||||
<Panel.Paragraph>
|
<AlertDialogBody>
|
||||||
This will shutdown the Ontime server. <br /> Are you sure?
|
This will shutdown the Ontime server. <br /> Are you sure?
|
||||||
</Panel.Paragraph>
|
</AlertDialogBody>
|
||||||
}
|
<AlertDialogFooter>
|
||||||
footerElements={
|
<Button ref={cancelRef} onClick={onClose} variant='ontime-ghosted-white'>
|
||||||
<>
|
Cancel
|
||||||
<Button ref={cancelRef} onClick={handler.close} variant='ghosted-white'>
|
</Button>
|
||||||
Cancel
|
<Button colorScheme='red' onClick={sendShutdown} disabled={!canShutdown}>
|
||||||
</Button>
|
Shutdown
|
||||||
<Button variant='destructive' onClick={sendShutdown} disabled={!canShutdown}>
|
</Button>
|
||||||
Shutdown
|
</AlertDialogFooter>
|
||||||
</Button>
|
</AlertDialogContent>
|
||||||
</>
|
</AlertDialogOverlay>
|
||||||
}
|
</AlertDialog>
|
||||||
/>
|
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import Info from '../../../../../common/components/info/Info';
|
import Info from '../../../../common/components/info/Info';
|
||||||
import ExternalLink from '../../../../../common/components/link/external-link/ExternalLink';
|
import ExternalLink from '../../../../common/components/link/external-link/ExternalLink';
|
||||||
|
|
||||||
const googleSheetDocsUrl = 'https://docs.getontime.no/features/import-spreadsheet-gsheet/';
|
const googleSheetDocsUrl = 'https://docs.getontime.no/features/import-spreadsheet-gsheet/';
|
||||||
|
|
||||||
+38
-17
@@ -1,13 +1,12 @@
|
|||||||
import { ChangeEvent, useEffect, useState } from 'react';
|
import { ChangeEvent, useEffect, useState } from 'react';
|
||||||
import { IoCheckmark, IoShieldCheckmarkOutline } from 'react-icons/io5';
|
import { IoCheckmark, IoShieldCheckmarkOutline } from 'react-icons/io5';
|
||||||
|
import { Button, Input, Spinner } from '@chakra-ui/react';
|
||||||
|
|
||||||
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 Button from '../../../../../common/components/buttons/Button';
|
import CopyTag from '../../../../common/components/copy-tag/CopyTag';
|
||||||
import CopyTag from '../../../../../common/components/copy-tag/CopyTag';
|
import { openLink } from '../../../../common/utils/linkUtils';
|
||||||
import Input from '../../../../../common/components/input/input/Input';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
import { openLink } from '../../../../../common/utils/linkUtils';
|
|
||||||
import * as Panel from '../../../panel-utils/PanelUtils';
|
|
||||||
|
|
||||||
import useGoogleSheet from './useGoogleSheet';
|
import useGoogleSheet from './useGoogleSheet';
|
||||||
import { useSheetStore } from './useSheetStore';
|
import { useSheetStore } from './useSheetStore';
|
||||||
@@ -139,33 +138,50 @@ export default function GSheetSetup(props: GSheetSetupProps) {
|
|||||||
<Panel.Title>
|
<Panel.Title>
|
||||||
Sync with Google Sheet (experimental)
|
Sync with Google Sheet (experimental)
|
||||||
{isAuthenticated ? (
|
{isAuthenticated ? (
|
||||||
<Button onClick={handleRevoke} loading={loading === 'cancel'}>
|
<Button variant='ontime-subtle' size='sm' onClick={handleRevoke} isLoading={loading === 'cancel'}>
|
||||||
Revoke Authentication
|
Revoke Authentication
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button onClick={handleCancelFlow}>Go Back</Button>
|
<Button variant='ontime-subtle' size='sm' onClick={handleCancelFlow}>
|
||||||
|
Go Back
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Panel.Title>
|
</Panel.Title>
|
||||||
<Panel.ListGroup>
|
<Panel.ListGroup>
|
||||||
<Panel.Description>Upload Client Secret provided by Google</Panel.Description>
|
<Panel.Description>Upload Client Secret provided by Google</Panel.Description>
|
||||||
<Panel.Error>{authenticationError}</Panel.Error>
|
<Panel.Error>{authenticationError}</Panel.Error>
|
||||||
<Input fluid type='file' onChange={handleClientSecret} accept='.json' disabled={isLoading || canAuthenticate} />
|
<Input
|
||||||
|
type='file'
|
||||||
|
onChange={handleClientSecret}
|
||||||
|
accept='.json'
|
||||||
|
size='sm'
|
||||||
|
variant='ontime-filled'
|
||||||
|
isDisabled={isLoading || canAuthenticate}
|
||||||
|
/>
|
||||||
</Panel.ListGroup>
|
</Panel.ListGroup>
|
||||||
<Panel.ListGroup>
|
<Panel.ListGroup>
|
||||||
<Panel.Description>Enter ID of sheet to synchronise</Panel.Description>
|
<Panel.Description>Enter ID of sheet to synchronise</Panel.Description>
|
||||||
<Panel.Error>{undefined}</Panel.Error>
|
<Panel.Error>{undefined}</Panel.Error>
|
||||||
<Input
|
<Input
|
||||||
fluid
|
size='sm'
|
||||||
|
variant='ontime-filled'
|
||||||
|
autoComplete='off'
|
||||||
placeholder='Sheet ID'
|
placeholder='Sheet ID'
|
||||||
onChange={(event) => setSheetId(event.target.value)}
|
onChange={(event) => setSheetId(event.target.value)}
|
||||||
disabled={isLoading || canAuthenticate}
|
isDisabled={isLoading || canAuthenticate}
|
||||||
/>
|
/>
|
||||||
</Panel.ListGroup>
|
</Panel.ListGroup>
|
||||||
{!canAuthenticate ? (
|
{!canAuthenticate ? (
|
||||||
<Panel.ListGroup>
|
<Panel.ListGroup>
|
||||||
<Panel.InlineElements>
|
<Panel.InlineElements>
|
||||||
<Button onClick={handleConnect} disabled={!canConnect || isLoading} loading={loading === 'connect'}>
|
<Button
|
||||||
<IoCheckmark />
|
variant='ontime-subtle'
|
||||||
|
size='sm'
|
||||||
|
leftIcon={<IoCheckmark />}
|
||||||
|
onClick={handleConnect}
|
||||||
|
isDisabled={!canConnect || isLoading}
|
||||||
|
isLoading={loading === 'connect'}
|
||||||
|
>
|
||||||
Connect
|
Connect
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
@@ -173,12 +189,17 @@ export default function GSheetSetup(props: GSheetSetupProps) {
|
|||||||
) : (
|
) : (
|
||||||
<Panel.ListGroup>
|
<Panel.ListGroup>
|
||||||
<Panel.InlineElements>
|
<Panel.InlineElements>
|
||||||
{isAuthenticating && <span>Authenticating...</span>}
|
{isAuthenticating && <Spinner />}
|
||||||
<CopyTag copyValue={authKey ?? ''} label='Google Auth Key' disabled={!canAuthenticate} size='sm'>
|
<CopyTag copyValue={authKey ?? ''} label='Google Auth Key' disabled={!canAuthenticate} size='sm'>
|
||||||
{authKey ? authKey : 'Upload files to generate Auth Key'}
|
{authKey ? authKey : 'Upload files to generate Auth Key'}
|
||||||
</CopyTag>
|
</CopyTag>
|
||||||
<Button onClick={handleAuthenticate} disabled={!canAuthenticate}>
|
<Button
|
||||||
<IoShieldCheckmarkOutline />
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
leftIcon={<IoShieldCheckmarkOutline />}
|
||||||
|
onClick={handleAuthenticate}
|
||||||
|
isDisabled={!canAuthenticate}
|
||||||
|
>
|
||||||
Authenticate
|
Authenticate
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { Button } from '@chakra-ui/react';
|
||||||
import { CustomFields, Rundown } from 'ontime-types';
|
import { CustomFields, Rundown } from 'ontime-types';
|
||||||
|
|
||||||
import Button from '../../../../../common/components/buttons/Button';
|
import * as Panel from '../../panel-utils/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';
|
||||||
@@ -44,10 +44,10 @@ export default function ImportReview(props: ImportReviewProps) {
|
|||||||
<Panel.Title>
|
<Panel.Title>
|
||||||
Review Rundown
|
Review Rundown
|
||||||
<Panel.InlineElements>
|
<Panel.InlineElements>
|
||||||
<Button onClick={handleCancel} variant='ghosted' disabled={loading}>
|
<Button onClick={handleCancel} variant='ontime-ghosted' size='sm' isDisabled={loading}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={applyImport} variant='primary' loading={loading}>
|
<Button onClick={applyImport} variant='ontime-filled' size='sm' isLoading={loading}>
|
||||||
Apply
|
Apply
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
+86
-75
@@ -1,18 +1,17 @@
|
|||||||
import { ChangeEvent, useRef, useState } from 'react';
|
import { ChangeEvent, useRef, useState } from 'react';
|
||||||
import { IoCloudOutline, IoDownloadOutline } from 'react-icons/io5';
|
import { IoCloudOutline, IoDownloadOutline } from 'react-icons/io5';
|
||||||
|
import { Button, Input } from '@chakra-ui/react';
|
||||||
import { getErrorMessage, ImportMap } from 'ontime-utils';
|
import { getErrorMessage, ImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getWorksheetNames as getWorksheetNamesExcel,
|
getWorksheetNames as getWorksheetNamesExcel,
|
||||||
importRundownPreview as importRundownPreviewExcel,
|
importRundownPreview as importRundownPreviewExcel,
|
||||||
upload as uploadExcel,
|
upload as uploadExcel,
|
||||||
} from '../../../../../common/api/excel';
|
} from '../../../../common/api/excel';
|
||||||
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 Button from '../../../../../common/components/buttons/Button';
|
import { validateExcelImport } from '../../../../common/utils/uploadUtils';
|
||||||
import * as Editor from '../../../../../common/components/editor-utils/EditorUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
import { validateExcelImport } from '../../../../../common/utils/uploadUtils';
|
|
||||||
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';
|
||||||
@@ -155,75 +154,87 @@ export default function SourcesPanel() {
|
|||||||
const showReview = rundown !== null && customFields !== null;
|
const showReview = rundown !== null && customFields !== null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel.Section>
|
<>
|
||||||
<Panel.Card>
|
<Panel.Header>Data sources</Panel.Header>
|
||||||
<Panel.SubHeader>Synchronise your rundown with an external source</Panel.SubHeader>
|
<Panel.Section>
|
||||||
{error && <Panel.Error>{error}</Panel.Error>}
|
<Panel.Card>
|
||||||
{showInput && (
|
<Panel.SubHeader>Synchronise your rundown with an external source</Panel.SubHeader>
|
||||||
<>
|
{error && <Panel.Error>{error}</Panel.Error>}
|
||||||
<GSheetInfo />
|
{showInput && (
|
||||||
<input
|
<>
|
||||||
ref={fileInputRef}
|
<GSheetInfo />
|
||||||
style={{ display: 'none' }}
|
<Input
|
||||||
type='file'
|
ref={fileInputRef}
|
||||||
onChange={handleFile}
|
style={{ display: 'none' }}
|
||||||
accept='.xlsx'
|
type='file'
|
||||||
data-testid='file-input'
|
onChange={handleFile}
|
||||||
/>
|
accept='.xlsx'
|
||||||
<div className={style.uploadSection}>
|
data-testid='file-input'
|
||||||
<div>
|
/>
|
||||||
<Button variant='primary' onClick={handleUpload} loading={hasFile === 'loading'}>
|
<div className={style.uploadSection}>
|
||||||
<IoDownloadOutline />
|
<div>
|
||||||
Import from spreadsheet
|
<Button
|
||||||
</Button>
|
variant='ontime-filled'
|
||||||
<Panel.Description>Accepts .xlsx files</Panel.Description>
|
size='sm'
|
||||||
</div>
|
leftIcon={<IoDownloadOutline />}
|
||||||
<Editor.Separator orientation='vertical' />
|
onClick={handleUpload}
|
||||||
<div>
|
isLoading={hasFile === 'loading'}
|
||||||
<Button variant='primary' onClick={openGSheetFlow} disabled={hasFile !== 'none'}>
|
>
|
||||||
<IoCloudOutline />
|
Import from spreadsheet
|
||||||
Synchronise with Google
|
</Button>
|
||||||
</Button>
|
<Panel.Description>Accepts .xlsx files</Panel.Description>
|
||||||
<Panel.Description>Start authentication process</Panel.Description>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
leftIcon={<IoCloudOutline />}
|
||||||
|
onClick={openGSheetFlow}
|
||||||
|
isDisabled={hasFile !== 'none'}
|
||||||
|
>
|
||||||
|
Synchronise with Google
|
||||||
|
</Button>
|
||||||
|
<Panel.Description>Start authentication process</Panel.Description>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{showCompleted && (
|
||||||
|
<div className={style.finishSection}>
|
||||||
|
{error ? (
|
||||||
|
<span key='finish__error' className={style.error}>
|
||||||
|
Import failed
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span key='finish__success' className={style.success}>
|
||||||
|
Import successful
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<Button variant='ontime-filled' size='sm' onClick={resetFlow}>
|
||||||
|
Return
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
)}
|
||||||
)}
|
{showAuth && <GSheetSetup onCancel={cancelGSheetFlow} />}
|
||||||
{showCompleted && (
|
{showImportMap && !showReview && (
|
||||||
<div className={style.finishSection}>
|
<ImportMapForm
|
||||||
{error ? (
|
hasErrors={Boolean(error)}
|
||||||
<span key='finish__error' className={style.error}>
|
isSpreadsheet={isExcelFlow}
|
||||||
Import failed
|
onCancel={cancelImportMap}
|
||||||
</span>
|
onSubmitExport={handleSubmitExport}
|
||||||
) : (
|
onSubmitImport={handleSubmitImportPreview}
|
||||||
<span key='finish__success' className={style.success}>
|
/>
|
||||||
Import successful
|
)}
|
||||||
</span>
|
{showReview && (
|
||||||
)}
|
<ImportReview
|
||||||
<Button variant='primary' onClick={resetFlow}>
|
rundown={rundown}
|
||||||
Return
|
customFields={customFields}
|
||||||
</Button>
|
onFinished={handleFinished}
|
||||||
</div>
|
onCancel={cancelImportMap}
|
||||||
)}
|
/>
|
||||||
{showAuth && <GSheetSetup onCancel={cancelGSheetFlow} />}
|
)}
|
||||||
{showImportMap && !showReview && (
|
</Panel.Card>
|
||||||
<ImportMapForm
|
</Panel.Section>
|
||||||
hasErrors={Boolean(error)}
|
</>
|
||||||
isSpreadsheet={isExcelFlow}
|
|
||||||
onCancel={cancelImportMap}
|
|
||||||
onSubmitExport={handleSubmitExport}
|
|
||||||
onSubmitImport={handleSubmitImportPreview}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{showReview && (
|
|
||||||
<ImportReview
|
|
||||||
rundown={rundown}
|
|
||||||
customFields={customFields}
|
|
||||||
onFinished={handleFinished}
|
|
||||||
onCancel={cancelImportMap}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Panel.Card>
|
|
||||||
</Panel.Section>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+28
-22
@@ -1,13 +1,10 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useFieldArray, useForm } from 'react-hook-form';
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
import { IoAdd, IoTrash } from 'react-icons/io5';
|
import { IoAdd, IoTrash } from 'react-icons/io5';
|
||||||
import { Select, Tooltip } from '@chakra-ui/react';
|
import { Button, IconButton, Input, Select, Tooltip } from '@chakra-ui/react';
|
||||||
import { ImportMap, isAlphanumericWithSpace } from 'ontime-utils';
|
import { ImportMap, isAlphanumericWithSpace } from 'ontime-utils';
|
||||||
|
|
||||||
import Button from '../../../../../../common/components/buttons/Button';
|
import * as Panel from '../../../panel-utils/PanelUtils';
|
||||||
import IconButton from '../../../../../../common/components/buttons/IconButton';
|
|
||||||
import Input from '../../../../../../common/components/input/input/Input';
|
|
||||||
import * as Panel from '../../../../panel-utils/PanelUtils';
|
|
||||||
import useGoogleSheet from '../useGoogleSheet';
|
import useGoogleSheet from '../useGoogleSheet';
|
||||||
import { useSheetStore } from '../useSheetStore';
|
import { useSheetStore } from '../useSheetStore';
|
||||||
|
|
||||||
@@ -98,29 +95,31 @@ export default function ImportMapForm(props: ImportMapFormProps) {
|
|||||||
<Panel.InlineElements>
|
<Panel.InlineElements>
|
||||||
{!isSpreadsheet && (
|
{!isSpreadsheet && (
|
||||||
<Tooltip label='Revoke the google authentication'>
|
<Tooltip label='Revoke the google authentication'>
|
||||||
<Button onClick={handleRevoke} disabled={isLoading}>
|
<Button variant='ontime-subtle' size='sm' onClick={handleRevoke} isDisabled={isLoading}>
|
||||||
Revoke
|
Revoke
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<Button onClick={onCancel} disabled={isLoading}>
|
<Button variant='ontime-subtle' size='sm' onClick={onCancel} isDisabled={isLoading}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
{!isSpreadsheet && (
|
{!isSpreadsheet && (
|
||||||
<Button
|
<Button
|
||||||
variant='primary'
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
onClick={handleSubmit(handleExport)}
|
onClick={handleSubmit(handleExport)}
|
||||||
disabled={!canSubmitGSheet}
|
isDisabled={!canSubmitGSheet}
|
||||||
loading={loading === 'export'}
|
isLoading={loading === 'export'}
|
||||||
>
|
>
|
||||||
Export
|
Export
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant='primary'
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
onClick={handleSubmit(handleImportPreview)}
|
onClick={handleSubmit(handleImportPreview)}
|
||||||
disabled={!canSubmit}
|
isDisabled={!canSubmit}
|
||||||
loading={loading === 'import'}
|
isLoading={loading === 'import'}
|
||||||
>
|
>
|
||||||
Import preview
|
Import preview
|
||||||
</Button>
|
</Button>
|
||||||
@@ -169,7 +168,9 @@ export default function ImportMapForm(props: ImportMapFormProps) {
|
|||||||
<td>
|
<td>
|
||||||
<Input
|
<Input
|
||||||
id={importName as string}
|
id={importName as string}
|
||||||
fluid
|
size='sm'
|
||||||
|
variant='ontime-filled'
|
||||||
|
autoComplete='off'
|
||||||
maxLength={25}
|
maxLength={25}
|
||||||
defaultValue={importName as string}
|
defaultValue={importName as string}
|
||||||
placeholder='Use default column name'
|
placeholder='Use default column name'
|
||||||
@@ -189,8 +190,10 @@ export default function ImportMapForm(props: ImportMapFormProps) {
|
|||||||
<tr key={key}>
|
<tr key={key}>
|
||||||
<td>
|
<td>
|
||||||
<Input
|
<Input
|
||||||
|
size='sm'
|
||||||
|
variant='ontime-filled'
|
||||||
|
autoComplete='off'
|
||||||
maxLength={25}
|
maxLength={25}
|
||||||
fluid
|
|
||||||
defaultValue={ontimeName}
|
defaultValue={ontimeName}
|
||||||
placeholder='Name of the field as shown in Ontime'
|
placeholder='Name of the field as shown in Ontime'
|
||||||
{...register(`custom.${index}.ontimeName`, {
|
{...register(`custom.${index}.ontimeName`, {
|
||||||
@@ -205,8 +208,10 @@ export default function ImportMapForm(props: ImportMapFormProps) {
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Input
|
<Input
|
||||||
|
size='sm'
|
||||||
|
variant='ontime-filled'
|
||||||
|
autoComplete='off'
|
||||||
maxLength={25}
|
maxLength={25}
|
||||||
fluid
|
|
||||||
defaultValue={importName}
|
defaultValue={importName}
|
||||||
placeholder='Name of the column in the spreadsheet'
|
placeholder='Name of the column in the spreadsheet'
|
||||||
{...register(`custom.${index}.importName`)}
|
{...register(`custom.${index}.importName`)}
|
||||||
@@ -214,12 +219,13 @@ export default function ImportMapForm(props: ImportMapFormProps) {
|
|||||||
</td>
|
</td>
|
||||||
<td className={style.singleActionCell}>
|
<td className={style.singleActionCell}>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant='ghosted-destructive'
|
size='sm'
|
||||||
|
variant='ontime-ghosted'
|
||||||
|
color='#FA5656' // $red-500
|
||||||
|
icon={<IoTrash />}
|
||||||
aria-label='Delete entry'
|
aria-label='Delete entry'
|
||||||
onClick={() => deleteCustomImport(index)}
|
onClick={() => deleteCustomImport(index)}
|
||||||
>
|
/>
|
||||||
<IoTrash />
|
|
||||||
</IconButton>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@@ -227,8 +233,8 @@ export default function ImportMapForm(props: ImportMapFormProps) {
|
|||||||
<tr>
|
<tr>
|
||||||
<td />
|
<td />
|
||||||
<Panel.InlineElements as='td' align='end'>
|
<Panel.InlineElements as='td' align='end'>
|
||||||
<Button onClick={addCustomImport}>
|
<Button size='sm' variant='ontime-subtle' rightIcon={<IoAdd />} onClick={addCustomImport}>
|
||||||
Add custom field <IoAdd />
|
Add custom field
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
<td />
|
<td />
|
||||||
+3
-3
@@ -3,9 +3,9 @@ import { IoLink } from 'react-icons/io5';
|
|||||||
import { CustomFields, isOntimeBlock, isOntimeEvent, Rundown } from 'ontime-types';
|
import { CustomFields, isOntimeBlock, isOntimeEvent, Rundown } from 'ontime-types';
|
||||||
import { millisToString } from 'ontime-utils';
|
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 '../../../../panel-utils/PanelUtils';
|
import * as Panel from '../../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import style from './PreviewRundown.module.scss';
|
import style from './PreviewRundown.module.scss';
|
||||||
|
|
||||||
+4
-4
@@ -2,16 +2,16 @@ import { useQueryClient } from '@tanstack/react-query';
|
|||||||
import { AuthenticationStatus, CustomFields, ProjectRundowns } from 'ontime-types';
|
import { AuthenticationStatus, CustomFields, ProjectRundowns } from 'ontime-types';
|
||||||
import { ImportMap } from 'ontime-utils';
|
import { ImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
import { CUSTOM_FIELDS, RUNDOWN } from '../../../../../common/api/constants';
|
import { CUSTOM_FIELDS, RUNDOWN } from '../../../../common/api/constants';
|
||||||
import { patchData } from '../../../../../common/api/db';
|
import { patchData } from '../../../../common/api/db';
|
||||||
import {
|
import {
|
||||||
previewRundown,
|
previewRundown,
|
||||||
requestConnection,
|
requestConnection,
|
||||||
revokeAuthentication,
|
revokeAuthentication,
|
||||||
uploadRundown,
|
uploadRundown,
|
||||||
verifyAuthenticationStatus,
|
verifyAuthenticationStatus,
|
||||||
} from '../../../../../common/api/sheets';
|
} from '../../../../common/api/sheets';
|
||||||
import { maybeAxiosError } from '../../../../../common/api/utils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
|
|
||||||
import { useSheetStore } from './useSheetStore';
|
import { useSheetStore } from './useSheetStore';
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.scrollContainer {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
@@ -1,35 +1,45 @@
|
|||||||
import { useForm } from 'react-hook-form';
|
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 { QuickStartData } from 'ontime-types';
|
||||||
import { parseUserTime } from 'ontime-utils';
|
import { parseUserTime } from 'ontime-utils';
|
||||||
|
|
||||||
import { quickProject } from '../../../common/api/db';
|
import { quickProject } from '../../../common/api/db';
|
||||||
import { invalidateAllCaches, maybeAxiosError } from '../../../common/api/utils';
|
import { invalidateAllCaches, maybeAxiosError } from '../../../common/api/utils';
|
||||||
import Button from '../../../common/components/buttons/Button';
|
|
||||||
import Input from '../../../common/components/input/input/Input';
|
|
||||||
import TimeInput from '../../../common/components/input/time-input/TimeInput';
|
import TimeInput from '../../../common/components/input/time-input/TimeInput';
|
||||||
import Modal from '../../../common/components/modal/Modal';
|
|
||||||
import Select from '../../../common/components/select/Select';
|
|
||||||
import Switch from '../../../common/components/switch/Switch';
|
|
||||||
import { editorSettingsDefaults, useEditorSettings } from '../../../common/stores/editorSettings';
|
import { editorSettingsDefaults, useEditorSettings } from '../../../common/stores/editorSettings';
|
||||||
import * as Panel from '../panel-utils/PanelUtils';
|
import * as Panel from '../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import { quickStartDefaults } from './quickStart.utils';
|
import { quickStartDefaults } from './quickStart.utils';
|
||||||
|
|
||||||
|
import style from './QuickStart.module.scss';
|
||||||
|
|
||||||
interface QuickStartProps {
|
interface QuickStartProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function QuickStart({ isOpen, onClose }: QuickStartProps) {
|
export default function QuickStart(props: QuickStartProps) {
|
||||||
|
const { isOpen, onClose } = props;
|
||||||
const { defaultWarnTime, defaultDangerTime, setDangerTime, setWarnTime } = useEditorSettings();
|
const { defaultWarnTime, defaultDangerTime, setDangerTime, setWarnTime } = useEditorSettings();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
register,
|
register,
|
||||||
formState: { errors, isSubmitting, isValid },
|
formState: { errors, isSubmitting, isValid },
|
||||||
watch,
|
|
||||||
setError,
|
setError,
|
||||||
setValue,
|
|
||||||
} = useForm<QuickStartData>({
|
} = useForm<QuickStartData>({
|
||||||
defaultValues: quickStartDefaults,
|
defaultValues: quickStartDefaults,
|
||||||
values: quickStartDefaults,
|
values: quickStartDefaults,
|
||||||
@@ -55,114 +65,123 @@ export default function QuickStart({ isOpen, onClose }: QuickStartProps) {
|
|||||||
const dangerTimeInMs = parseUserTime(defaultDangerTime);
|
const dangerTimeInMs = parseUserTime(defaultDangerTime);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal isOpen={isOpen} onClose={onClose} closeOnOverlayClick={false} variant='ontime'>
|
||||||
isOpen={isOpen}
|
<ModalOverlay />
|
||||||
onClose={onClose}
|
<ModalCloseButton />
|
||||||
showBackdrop
|
<ModalContent maxWidth='max(640px, 40vw)'>
|
||||||
showCloseButton
|
|
||||||
title='Create new project...'
|
|
||||||
bodyElements={
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} id='quick-start'>
|
<form onSubmit={handleSubmit(onSubmit)} id='quick-start'>
|
||||||
<Panel.ListGroup>
|
<ModalHeader>Create new project...</ModalHeader>
|
||||||
<Panel.ListItem>
|
<ModalBody className={style.scrollContainer}>
|
||||||
<Panel.Field title='Project title' description='Shown as the title in some views' />
|
<ModalCloseButton />
|
||||||
<Input maxLength={150} placeholder='Project title' fluid {...register('project.title')} />
|
<Panel.ListGroup>
|
||||||
</Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.ListItem>
|
<Panel.Field title='Project title' description='Shown as the title in some views' />
|
||||||
<Panel.Field
|
<Input
|
||||||
title='Time format'
|
variant='ontime-filled'
|
||||||
description='Default time format to show in views 12 /24 hours'
|
size='sm'
|
||||||
error={errors.settings?.timeFormat?.message}
|
maxLength={150}
|
||||||
/>
|
placeholder='Project title'
|
||||||
<Select
|
autoComplete='off'
|
||||||
{...register('settings.timeFormat')}
|
width='20rem'
|
||||||
defaultValue='24'
|
{...register('project.title')}
|
||||||
options={[
|
/>
|
||||||
{ value: '12', label: '12 hours 11:00:10 PM' },
|
</Panel.ListItem>
|
||||||
{ value: '24', label: '24 hours 23:00:10' },
|
<Panel.ListItem>
|
||||||
]}
|
<Panel.Field
|
||||||
/>
|
title='Time format'
|
||||||
</Panel.ListItem>
|
description='Default time format to show in views 12 /24 hours'
|
||||||
<Panel.ListItem>
|
error={errors.settings?.timeFormat?.message}
|
||||||
<Panel.Field
|
/>
|
||||||
title='Views language'
|
<Select variant='ontime' size='sm' width='auto' isDisabled={false} {...register('settings.timeFormat')}>
|
||||||
description='Language to be displayed in views'
|
<option value='12'>12 hours 11:00:10 PM</option>
|
||||||
error={errors.settings?.language?.message}
|
<option value='24'>24 hours 23:00:10</option>
|
||||||
/>
|
</Select>
|
||||||
<Select
|
</Panel.ListItem>
|
||||||
{...register('settings.language')}
|
<Panel.ListItem>
|
||||||
defaultValue='en'
|
<Panel.Field
|
||||||
options={[
|
title='Views language'
|
||||||
{ value: 'en', label: 'English' },
|
description='Language to be displayed in views'
|
||||||
{ value: 'fr', label: 'French' },
|
error={errors.settings?.language?.message}
|
||||||
{ value: 'de', label: 'German' },
|
/>
|
||||||
{ value: 'hu', label: 'Hungarian' },
|
<Select variant='ontime' size='sm' width='auto' isDisabled={false} {...register('settings.language')}>
|
||||||
{ value: 'it', label: 'Italian' },
|
<option value='en'>English</option>
|
||||||
{ value: 'no', label: 'Norwegian' },
|
<option value='fr'>French</option>
|
||||||
{ value: 'pt', label: 'Portuguese' },
|
<option value='de'>German</option>
|
||||||
{ value: 'es', label: 'Spanish' },
|
<option value='hu'>Hungarian</option>
|
||||||
{ value: 'sv', label: 'Swedish' },
|
<option value='it'>Italian</option>
|
||||||
{ value: 'pl', label: 'Polish' },
|
<option value='no'>Norwegian</option>
|
||||||
{ value: 'zh', label: 'Chinese (Simplified)' },
|
<option value='pt'>Portuguese</option>
|
||||||
]}
|
<option value='es'>Spanish</option>
|
||||||
/>
|
<option value='sv'>Swedish</option>
|
||||||
</Panel.ListItem>
|
<option value='pl'>Polish</option>
|
||||||
</Panel.ListGroup>
|
<option value='zh'>Chinese (Simplified)</option>
|
||||||
|
</Select>
|
||||||
|
</Panel.ListItem>
|
||||||
|
</Panel.ListGroup>
|
||||||
|
|
||||||
<Panel.ListGroup>
|
<Panel.ListGroup>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field title='Warning time' description='Default threshold for warning time in an event' />
|
<Panel.Field title='Warning time' description='Default threshold for warning time in an event' />
|
||||||
<TimeInput<'warnTime'>
|
<TimeInput<'warnTime'>
|
||||||
name='warnTime'
|
name='warnTime'
|
||||||
submitHandler={(_field, value) => setWarnTime(value)}
|
submitHandler={(_field, value) => setWarnTime(value)}
|
||||||
time={warnTimeInMs}
|
time={warnTimeInMs}
|
||||||
placeholder={editorSettingsDefaults.warnTime}
|
placeholder={editorSettingsDefaults.warnTime}
|
||||||
/>
|
/>
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field title='Danger time' description='Default threshold for danger time in an event' />
|
<Panel.Field title='Danger time' description='Default threshold for danger time in an event' />
|
||||||
<TimeInput<'dangerTime'>
|
<TimeInput<'dangerTime'>
|
||||||
name='dangerTime'
|
name='dangerTime'
|
||||||
submitHandler={(_field, value) => setDangerTime(value)}
|
submitHandler={(_field, value) => setDangerTime(value)}
|
||||||
time={dangerTimeInMs}
|
time={dangerTimeInMs}
|
||||||
placeholder={editorSettingsDefaults.dangerTime}
|
placeholder={editorSettingsDefaults.dangerTime}
|
||||||
/>
|
/>
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
</Panel.ListGroup>
|
</Panel.ListGroup>
|
||||||
|
|
||||||
<Panel.ListGroup>
|
<Panel.ListGroup>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field
|
<Panel.Field
|
||||||
title='Freeze timer on end'
|
title='Freeze timer on end'
|
||||||
description='When a timer hits 00:00:00, it freezes instead of going negative. It invalidates the End Message.'
|
description='When a timer hits 00:00:00, it freezes instead of going negative. It invalidates the End Message.'
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Controller
|
||||||
name='viewSettings.freezeEnd'
|
control={control}
|
||||||
checked={watch('viewSettings.freezeEnd')}
|
name='viewSettings.freezeEnd'
|
||||||
onCheckedChange={(checked) => setValue('viewSettings.freezeEnd', checked)}
|
render={({ field: { onChange, value, ref } }) => (
|
||||||
/>
|
<Switch variant='ontime' size='lg' isChecked={value} onChange={onChange} ref={ref} />
|
||||||
</Panel.ListItem>
|
)}
|
||||||
<Panel.ListItem>
|
/>
|
||||||
<Panel.Field
|
</Panel.ListItem>
|
||||||
title='End message'
|
<Panel.ListItem>
|
||||||
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'
|
<Panel.Field
|
||||||
/>
|
title='End message'
|
||||||
<Input maxLength={150} fluid placeholder='eg: Time is up!' {...register('viewSettings.endMessage')} />
|
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'
|
||||||
</Panel.ListItem>
|
/>
|
||||||
</Panel.ListGroup>
|
<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>
|
</form>
|
||||||
}
|
</ModalContent>
|
||||||
footerElements={
|
</Modal>
|
||||||
<>
|
|
||||||
{errors?.root && <Panel.Error>{errors.root.message}</Panel.Error>}
|
|
||||||
<Button variant='ghosted' onClick={onClose} disabled={false}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button variant='primary' type='submit' form='quick-start' disabled={!isValid} loading={isSubmitting}>
|
|
||||||
Create project
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,63 +11,60 @@ export type SettingsOption = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const staticOptions = [
|
const staticOptions = [
|
||||||
{
|
|
||||||
id: 'settings',
|
|
||||||
label: 'Settings',
|
|
||||||
secondary: [
|
|
||||||
{ id: 'settings__data', label: 'Project data' },
|
|
||||||
{ id: 'settings__general', label: 'General settings' },
|
|
||||||
{ id: 'settings__view', label: 'View settings' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'project',
|
id: 'project',
|
||||||
label: 'Project',
|
label: 'Project',
|
||||||
split: true,
|
|
||||||
secondary: [
|
secondary: [
|
||||||
{ id: 'project__create', label: 'Create...' },
|
{ id: 'project__create', label: 'Create...' },
|
||||||
{ id: 'project__list', label: 'Manage projects' },
|
{ id: 'project__data', label: 'Project data' },
|
||||||
|
{ id: 'project__manage', label: 'Manage projects' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'manage',
|
id: 'general',
|
||||||
label: 'Project data',
|
label: 'App Settings',
|
||||||
secondary: [
|
secondary: [
|
||||||
{ id: 'manage__defaults', label: 'Rundown defaults' },
|
{ id: 'general__settings', label: 'General settings' },
|
||||||
{ id: 'manage__custom', label: 'Custom fields' },
|
{ id: 'general__editor', label: 'Editor settings' },
|
||||||
{ id: 'manage__rundowns', label: 'Manage rundowns' },
|
{ id: 'general__view', label: 'View settings' },
|
||||||
{ id: 'manage__sheets', label: 'Import spreadsheet' },
|
|
||||||
{ id: 'manage__sheets', label: 'Sync with Google Sheet' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'feature_settings',
|
||||||
|
label: 'Feature Settings',
|
||||||
|
secondary: [
|
||||||
|
{ id: 'feature_settings__custom', label: 'Custom fields' },
|
||||||
|
{ id: 'feature_settings__urlpresets', label: 'URL Presets' },
|
||||||
|
{ id: 'feature_settings__report', label: 'Report' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sources',
|
||||||
|
label: 'Data Sources',
|
||||||
|
secondary: [
|
||||||
|
{ id: 'sources__xlsx', label: 'Import spreadsheet' },
|
||||||
|
{ id: 'sources__gsheet', label: 'Sync with Google Sheet' },
|
||||||
|
],
|
||||||
|
split: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'automation',
|
id: 'automation',
|
||||||
label: 'Automation',
|
label: 'Automation',
|
||||||
split: true,
|
|
||||||
secondary: [
|
secondary: [
|
||||||
{ id: 'automation__settings', label: 'Automation settings' },
|
{ id: 'automation__settings', label: 'Automation settings' },
|
||||||
{ id: 'automation__automations', label: 'Manage automations' },
|
{ id: 'automation__automations', label: 'Manage automations' },
|
||||||
{ id: 'automation__triggers', label: 'Manage triggers' },
|
{ id: 'automation__triggers', label: 'Manage triggers' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'sharing',
|
|
||||||
label: 'Sharing and reporting',
|
|
||||||
split: true,
|
|
||||||
secondary: [
|
|
||||||
{ id: 'sharing__presets', label: 'URL Presets' },
|
|
||||||
{
|
|
||||||
id: 'sharing__link',
|
|
||||||
label: 'Share link',
|
|
||||||
},
|
|
||||||
{ id: 'sharing__report', label: 'Runtime report' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'network',
|
id: 'network',
|
||||||
label: 'Network',
|
label: 'Network',
|
||||||
split: true,
|
split: true,
|
||||||
secondary: [
|
secondary: [
|
||||||
|
{
|
||||||
|
id: 'network__link',
|
||||||
|
label: 'Share link',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'network__log',
|
id: 'network__log',
|
||||||
label: 'Event log',
|
label: 'Event log',
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user