mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-21 06:59:09 +00:00
refactor: migrate UI components and remove chakra
migrate pin page migrate copy tag migrate pin input migrate dropdown menus migrate radio buttons migrate switch migrate select migrate textarea migrate colour picker migrate select migrate context menu migrate viewparams remove chakra
This commit is contained in:
committed by
Carlos Valente
parent
4d359445a7
commit
e1e8410ba4
@@ -5,13 +5,10 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@base-ui-components/react": "1.0.0-beta.1",
|
"@base-ui-components/react": "1.0.0-beta.1",
|
||||||
"@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",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@emotion/is-prop-valid": "^1.3.1",
|
"@emotion/is-prop-valid": "^1.3.1",
|
||||||
"@emotion/react": "^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": "^8.1.2",
|
||||||
"@sentry/react": "^8.43.0",
|
"@sentry/react": "^8.43.0",
|
||||||
@@ -22,7 +19,6 @@
|
|||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"csv-stringify": "^6.4.5",
|
"csv-stringify": "^6.4.5",
|
||||||
"framer-motion": "^10.10.0",
|
|
||||||
"prismjs": "^1.29.0",
|
"prismjs": "^1.29.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
@@ -80,7 +76,6 @@
|
|||||||
"eslint-plugin-react-compiler": "19.1.0-rc.2",
|
"eslint-plugin-react-compiler": "19.1.0-rc.2",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-simple-import-sort": "^8.0.0",
|
"eslint-plugin-simple-import-sort": "^8.0.0",
|
||||||
"happy-dom": "^16.7.2",
|
|
||||||
"ontime-types": "workspace:*",
|
"ontime-types": "workspace:*",
|
||||||
"ontime-utils": "workspace:*",
|
"ontime-utils": "workspace:*",
|
||||||
"prettier": "catalog:",
|
"prettier": "catalog:",
|
||||||
|
|||||||
+18
-22
@@ -1,6 +1,5 @@
|
|||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import { Tooltip } from '@base-ui-components/react/tooltip';
|
import { Tooltip } from '@base-ui-components/react/tooltip';
|
||||||
import { ChakraProvider } from '@chakra-ui/react';
|
|
||||||
import { QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
|
|
||||||
@@ -9,7 +8,6 @@ import IdentifyOverlay from './common/components/identify-overlay/IdentifyOverla
|
|||||||
import { AppContextProvider } from './common/context/AppContext';
|
import { AppContextProvider } from './common/context/AppContext';
|
||||||
import { ontimeQueryClient } from './common/queryClient';
|
import { ontimeQueryClient } from './common/queryClient';
|
||||||
import { connectSocket } from './common/utils/socket';
|
import { connectSocket } from './common/utils/socket';
|
||||||
import theme from './theme/theme';
|
|
||||||
import { TranslationProvider } from './translation/TranslationProvider';
|
import { TranslationProvider } from './translation/TranslationProvider';
|
||||||
import AppRouter from './AppRouter';
|
import AppRouter from './AppRouter';
|
||||||
import { baseURI } from './externals';
|
import { baseURI } from './externals';
|
||||||
@@ -18,28 +16,26 @@ connectSocket();
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<ChakraProvider disableGlobalStyle resetCSS theme={theme}>
|
<QueryClientProvider client={ontimeQueryClient}>
|
||||||
<QueryClientProvider client={ontimeQueryClient}>
|
<AppContextProvider>
|
||||||
<AppContextProvider>
|
<Tooltip.Provider>
|
||||||
<Tooltip.Provider>
|
<BrowserRouter basename={baseURI}>
|
||||||
<BrowserRouter basename={baseURI}>
|
<div className='App'>
|
||||||
<div className='App'>
|
|
||||||
<ErrorBoundary>
|
|
||||||
<TranslationProvider>
|
|
||||||
<IdentifyOverlay />
|
|
||||||
<AppRouter />
|
|
||||||
</TranslationProvider>
|
|
||||||
</ErrorBoundary>
|
|
||||||
<ReactQueryDevtools initialIsOpen={false} />
|
|
||||||
</div>
|
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<div id='identify-portal' />
|
<TranslationProvider>
|
||||||
|
<IdentifyOverlay />
|
||||||
|
<AppRouter />
|
||||||
|
</TranslationProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</BrowserRouter>
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
</Tooltip.Provider>
|
</div>
|
||||||
</AppContextProvider>
|
<ErrorBoundary>
|
||||||
</QueryClientProvider>
|
<div id='identify-portal' />
|
||||||
</ChakraProvider>
|
</ErrorBoundary>
|
||||||
|
</BrowserRouter>
|
||||||
|
</Tooltip.Provider>
|
||||||
|
</AppContextProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
.contextMenuButton {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contextMenuBackdrop {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
// logic (with some modifications) culled from:
|
|
||||||
// https://github.com/lukasbach/chakra-ui-contextmenu/blob/main/src/ContextMenu.tsx
|
|
||||||
|
|
||||||
import { ReactElement } from 'react';
|
|
||||||
import { IconType } from 'react-icons';
|
|
||||||
import { Menu, MenuButton, MenuGroup, MenuList } from '@chakra-ui/react';
|
|
||||||
import { create } from 'zustand';
|
|
||||||
|
|
||||||
import { ContextMenuOption } from './ContextMenuOption';
|
|
||||||
|
|
||||||
import style from './ContextMenu.module.scss';
|
|
||||||
|
|
||||||
type ContextMenuCoords = {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type OptionWithoutGroup = {
|
|
||||||
label: string;
|
|
||||||
isDisabled?: boolean;
|
|
||||||
icon: IconType;
|
|
||||||
onClick: () => void;
|
|
||||||
withDivider?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type OptionWithGroup = {
|
|
||||||
label: string;
|
|
||||||
group: Omit<OptionWithoutGroup, 'isGroup'>[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Option = OptionWithoutGroup | OptionWithGroup;
|
|
||||||
|
|
||||||
const isOptionWithGroup = (option: Option): option is OptionWithGroup => 'group' in option;
|
|
||||||
|
|
||||||
type ContextMenuStore = {
|
|
||||||
coords: ContextMenuCoords;
|
|
||||||
options: Option[];
|
|
||||||
isOpen: boolean;
|
|
||||||
setContextMenu: (coords: ContextMenuCoords, options: Option[]) => void;
|
|
||||||
setIsOpen: (newIsOpen: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useContextMenuStore = create<ContextMenuStore>((set) => ({
|
|
||||||
coords: { x: 0, y: 0 },
|
|
||||||
options: [],
|
|
||||||
isOpen: false,
|
|
||||||
setContextMenu: (coords, options) => set(() => ({ coords, options, isOpen: true })),
|
|
||||||
setIsOpen: (newIsOpen) => set(() => ({ isOpen: newIsOpen })),
|
|
||||||
}));
|
|
||||||
|
|
||||||
interface ContextMenuProps {
|
|
||||||
// ReactElement type required due to early `return` (line 51) returning {children}
|
|
||||||
children: ReactElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ContextMenu = ({ children }: ContextMenuProps) => {
|
|
||||||
const { coords, options, isOpen, setIsOpen } = useContextMenuStore();
|
|
||||||
|
|
||||||
const onClose = () => {
|
|
||||||
return setIsOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isOpen) {
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{children}
|
|
||||||
<div className={style.contextMenuBackdrop} />
|
|
||||||
<Menu isOpen size='sm' gutter={0} onClose={onClose} isLazy lazyBehavior='unmount' variant='ontime-on-dark'>
|
|
||||||
<MenuButton
|
|
||||||
className={style.contextMenuButton}
|
|
||||||
aria-hidden
|
|
||||||
w={1}
|
|
||||||
h={1}
|
|
||||||
style={{
|
|
||||||
position: 'fixed',
|
|
||||||
left: coords.x,
|
|
||||||
top: coords.y,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<MenuList>
|
|
||||||
{options.map((option) =>
|
|
||||||
isOptionWithGroup(option) ? (
|
|
||||||
<MenuGroup key={option.label} title={option.label}>
|
|
||||||
{option.group.map((groupOption) => (
|
|
||||||
<ContextMenuOption key={groupOption.label} {...groupOption} />
|
|
||||||
))}
|
|
||||||
</MenuGroup>
|
|
||||||
) : (
|
|
||||||
<ContextMenuOption key={option.label} {...option} />
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</MenuList>
|
|
||||||
</Menu>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { MenuDivider, MenuItem } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
import { OptionWithoutGroup } from './ContextMenu';
|
|
||||||
|
|
||||||
export const ContextMenuOption = ({ label, onClick, isDisabled, icon: Icon, withDivider }: OptionWithoutGroup) => (
|
|
||||||
<>
|
|
||||||
{withDivider && <MenuDivider />}
|
|
||||||
<MenuItem icon={<Icon style={{ fontSize: '1rem' }} />} onClick={onClick} isDisabled={isDisabled}>
|
|
||||||
{label}
|
|
||||||
</MenuItem>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
.copytag {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
border-radius: 3px 0 0 3px;
|
||||||
|
border: 1px solid $gray-1200;
|
||||||
|
background-color: $white-3;
|
||||||
|
padding-inline: 0.5rem 1rem;
|
||||||
|
line-height: 1em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: $label-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
height: 1.5rem;
|
||||||
|
font-size: calc(1rem - 3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.medium {
|
||||||
|
height: 2rem;
|
||||||
|
font-size: calc(1rem - 2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.large {
|
||||||
|
height: 2.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
@@ -1,22 +1,28 @@
|
|||||||
import { PropsWithChildren, useState } from 'react';
|
import { PropsWithChildren, useState } from 'react';
|
||||||
import { IoCheckmark } from 'react-icons/io5';
|
import { IoCheckmark } from 'react-icons/io5';
|
||||||
import { IoCopy } from 'react-icons/io5';
|
import { IoCopy } from 'react-icons/io5';
|
||||||
import { Button, ButtonGroup, IconButton, Tooltip } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
import { tooltipDelayFast } from '../../../ontimeConfig';
|
|
||||||
import { Size } from '../../models/Util.type';
|
|
||||||
import copyToClipboard from '../../utils/copyToClipboard';
|
import copyToClipboard from '../../utils/copyToClipboard';
|
||||||
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
import Button from '../buttons/Button';
|
||||||
|
import IconButton from '../buttons/IconButton';
|
||||||
|
|
||||||
|
import style from './CopyTag.module.scss';
|
||||||
|
|
||||||
interface CopyTagProps {
|
interface CopyTagProps {
|
||||||
copyValue: string;
|
copyValue: string;
|
||||||
label: string;
|
|
||||||
size?: Size;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
size?: 'small' | 'medium' | 'large';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CopyTag(props: PropsWithChildren<CopyTagProps>) {
|
export default function CopyTag({
|
||||||
const { copyValue, label, size = 'xs', disabled, children, onClick } = props;
|
copyValue,
|
||||||
|
disabled,
|
||||||
|
size = 'medium',
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
}: PropsWithChildren<CopyTagProps>) {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
@@ -28,20 +34,24 @@ export default function CopyTag(props: PropsWithChildren<CopyTagProps>) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip label={label} openDelay={tooltipDelayFast}>
|
<div className={style.copytag}>
|
||||||
<ButtonGroup size={size} isAttached>
|
{onClick !== undefined ? (
|
||||||
<Button variant='ontime-subtle' tabIndex={-1} onClick={onClick} isDisabled={disabled}>
|
<Button className={style.action} size={size} tabIndex={-1} onClick={onClick} disabled={disabled}>
|
||||||
{children}
|
{children}
|
||||||
</Button>
|
</Button>
|
||||||
<IconButton
|
) : (
|
||||||
aria-label={label}
|
<div className={cx([style.label, style[size]])}>{children}</div>
|
||||||
icon={copied ? <IoCheckmark /> : <IoCopy />}
|
)}
|
||||||
variant='ontime-filled'
|
<IconButton
|
||||||
tabIndex={-1}
|
className={style.copy}
|
||||||
onClick={handleClick}
|
variant='primary'
|
||||||
isDisabled={disabled}
|
size={size}
|
||||||
/>
|
tabIndex={-1}
|
||||||
</ButtonGroup>
|
onClick={handleClick}
|
||||||
</Tooltip>
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{copied ? <IoCheckmark /> : <IoCopy />}
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
top: 10%;
|
top: 10%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
|
||||||
z-index: $zindex-dialog;
|
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
|
|
||||||
padding-inline: 1rem;
|
padding-inline: 1rem;
|
||||||
@@ -19,7 +18,6 @@
|
|||||||
.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);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { PropsWithChildren, ReactNode } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
|
import { IconType } from 'react-icons';
|
||||||
import { Menu as BaseMenu } from '@base-ui-components/react/menu';
|
import { Menu as BaseMenu } from '@base-ui-components/react/menu';
|
||||||
|
|
||||||
import style from './DropdownMenu.module.scss';
|
import style from './DropdownMenu.module.scss';
|
||||||
@@ -7,13 +8,15 @@ type DropdownMenuItemDivider = { type: 'divider' };
|
|||||||
type DropdownMenuItem = {
|
type DropdownMenuItem = {
|
||||||
type: 'item';
|
type: 'item';
|
||||||
label: string;
|
label: string;
|
||||||
icon?: ReactNode;
|
icon?: IconType;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DropdownMenuOption = DropdownMenuItemDivider | DropdownMenuItem;
|
||||||
|
|
||||||
interface DropdownMenuProps extends BaseMenu.Trigger.Props {
|
interface DropdownMenuProps extends BaseMenu.Trigger.Props {
|
||||||
items: Array<DropdownMenuItemDivider | DropdownMenuItem>;
|
items: DropdownMenuOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DropdownMenu({ items, children, ...triggerProps }: PropsWithChildren<DropdownMenuProps>) {
|
export function DropdownMenu({ items, children, ...triggerProps }: PropsWithChildren<DropdownMenuProps>) {
|
||||||
@@ -29,7 +32,8 @@ export function DropdownMenu({ items, children, ...triggerProps }: PropsWithChil
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<BaseMenu.Item key={index} className={style.item} onClick={item.onClick} disabled={item.disabled}>
|
<BaseMenu.Item key={index} className={style.item} onClick={item.onClick} disabled={item.disabled}>
|
||||||
{item.icon} {item.label}
|
{item.icon && <item.icon />}
|
||||||
|
{item.label}
|
||||||
</BaseMenu.Item>
|
</BaseMenu.Item>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -55,12 +59,9 @@ export function PositionedDropdownMenu({ items, isOpen, position, onClose }: Pos
|
|||||||
if (!open) onClose();
|
if (!open) onClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BaseMenu.Trigger
|
<BaseMenu.Trigger style={{ position: 'fixed', left: position.x, top: position.y }} aria-hidden />
|
||||||
style={{ position: 'absolute', left: position.x, top: position.y, pointerEvents: 'none' }}
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
<BaseMenu.Portal>
|
<BaseMenu.Portal>
|
||||||
<BaseMenu.Positioner className={style.positioner} align='start' sideOffset={8}>
|
<BaseMenu.Positioner className={style.positioner} align='start' sideOffset={8} alignOffset={8}>
|
||||||
<BaseMenu.Popup className={style.popup}>
|
<BaseMenu.Popup className={style.popup}>
|
||||||
{items.map((item, index) => {
|
{items.map((item, index) => {
|
||||||
if (item.type === 'divider') {
|
if (item.type === 'divider') {
|
||||||
@@ -68,7 +69,8 @@ export function PositionedDropdownMenu({ items, isOpen, position, onClose }: Pos
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<BaseMenu.Item key={index} className={style.item} onClick={item.onClick} disabled={item.disabled}>
|
<BaseMenu.Item key={index} className={style.item} onClick={item.onClick} disabled={item.disabled}>
|
||||||
{item.icon} {item.label}
|
{item.icon && <item.icon />}
|
||||||
|
{item.label}
|
||||||
</BaseMenu.Item>
|
</BaseMenu.Item>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
place-content: center;
|
place-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
z-index: $zindex-modal;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { RefObject, useEffect } from 'react';
|
|
||||||
import { Textarea, TextareaProps } from '@chakra-ui/react';
|
|
||||||
// @ts-expect-error no types from library
|
|
||||||
import autosize from 'autosize/dist/autosize';
|
|
||||||
|
|
||||||
export const AutoTextArea = (props: TextareaProps & { inputref: RefObject<unknown> }) => {
|
|
||||||
const { value, inputref } = props;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const node = inputref.current;
|
|
||||||
autosize(inputref.current);
|
|
||||||
return () => {
|
|
||||||
autosize.destroy(node);
|
|
||||||
};
|
|
||||||
}, [inputref, value]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Textarea
|
|
||||||
overflow='hidden'
|
|
||||||
w='100%'
|
|
||||||
ref={inputref}
|
|
||||||
resize='none'
|
|
||||||
transition='height none'
|
|
||||||
variant='ontime-transparent'
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { RefObject, useEffect } from 'react';
|
||||||
|
// @ts-expect-error no types from library
|
||||||
|
import autosize from 'autosize/dist/autosize';
|
||||||
|
|
||||||
|
import Textarea, { type TextareaProps } from '../textarea/Textarea';
|
||||||
|
|
||||||
|
interface AutoTextAreaProps extends TextareaProps {
|
||||||
|
inputref: RefObject<HTMLTextAreaElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A textarea that automatically resizes based on its content
|
||||||
|
*/
|
||||||
|
export function AutoTextarea({ value, inputref, ...textAreaProps }: AutoTextAreaProps) {
|
||||||
|
// when the value changes, we use the ref to reapply autosize
|
||||||
|
useEffect(() => {
|
||||||
|
const node = inputref.current;
|
||||||
|
autosize(inputref.current);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
autosize.destroy(node);
|
||||||
|
};
|
||||||
|
}, [inputref, value]);
|
||||||
|
|
||||||
|
return <Textarea ref={inputref} value={value} {...textAreaProps} />;
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useCallback } from 'react';
|
|
||||||
import { useController, UseControllerProps } from 'react-hook-form';
|
import { useController, UseControllerProps } from 'react-hook-form';
|
||||||
import { IoEyedrop } from 'react-icons/io5';
|
import { IoEyedrop } from 'react-icons/io5';
|
||||||
|
import { useDebouncedCallback } from '@mantine/hooks';
|
||||||
import { ViewSettings } from 'ontime-types';
|
import { ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import { debounce } from '../../../utils/debounce';
|
|
||||||
import { cx, getAccessibleColour } from '../../../utils/styleUtils';
|
import { cx, getAccessibleColour } from '../../../utils/styleUtils';
|
||||||
import PopoverPicker from '../popover-picker/PopoverPicker';
|
import PopoverPicker from '../popover-picker/PopoverPicker';
|
||||||
|
|
||||||
@@ -19,12 +18,9 @@ interface SwatchPickerProps {
|
|||||||
export default function SwatchPicker(props: SwatchPickerProps) {
|
export default function SwatchPicker(props: SwatchPickerProps) {
|
||||||
const { color, onChange, isSelected, alwaysDisplayColor } = props;
|
const { color, onChange, isSelected, alwaysDisplayColor } = props;
|
||||||
|
|
||||||
const debouncedOnChange = useCallback(
|
const debouncedOnChange = useDebouncedCallback((newValue: string) => {
|
||||||
debounce((newValue: string) => {
|
onChange(newValue);
|
||||||
onChange(newValue);
|
}, 100);
|
||||||
}, 500),
|
|
||||||
[onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const displayColor = alwaysDisplayColor || isSelected ? color : '';
|
const displayColor = alwaysDisplayColor || isSelected ? color : '';
|
||||||
const { color: iconColor } = getAccessibleColour(displayColor);
|
const { color: iconColor } = getAccessibleColour(displayColor);
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
.radioGroup {
|
||||||
|
color: $gray-900;
|
||||||
|
font-size: calc(1rem - 3px);
|
||||||
|
color: $label-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
line-height: 1.2em;
|
||||||
|
|
||||||
|
&:has([data-checked]) {
|
||||||
|
color: $ui-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
width: 0.75rem;
|
||||||
|
height: 0.75rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 100%;
|
||||||
|
outline: 0;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&[data-unchecked] {
|
||||||
|
background-color: $gray-1200;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-checked] {
|
||||||
|
background-color: $gray-1200;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $gray-1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid $blue-500;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
|
||||||
|
&[data-unchecked] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
border-radius: 100%;
|
||||||
|
width: 0.5em;
|
||||||
|
height: 0.5em;
|
||||||
|
background-color: $blue-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Radio } from '@base-ui-components/react/radio';
|
||||||
|
import { RadioGroup as BaseRadioGroup } from '@base-ui-components/react/radio-group';
|
||||||
|
|
||||||
|
import style from './BlockRadio.module.scss';
|
||||||
|
|
||||||
|
interface BlockRadioProps<T extends string | number | boolean> extends Omit<BaseRadioGroup.Props, 'onValueChange'> {
|
||||||
|
items: {
|
||||||
|
value: T;
|
||||||
|
label: string;
|
||||||
|
}[];
|
||||||
|
onValueChange?: (value: T) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BlockRadio<T extends string | number | boolean>({
|
||||||
|
items,
|
||||||
|
onValueChange,
|
||||||
|
...elementProps
|
||||||
|
}: BlockRadioProps<T>) {
|
||||||
|
return (
|
||||||
|
<BaseRadioGroup
|
||||||
|
onValueChange={(value) => onValueChange?.(value as T)}
|
||||||
|
className={style.radioGroup}
|
||||||
|
{...elementProps}
|
||||||
|
>
|
||||||
|
{items.map((item) => (
|
||||||
|
<label className={style.item} key={item.value.toString()}>
|
||||||
|
<Radio.Root value={item.value.toString()} className={style.radio}>
|
||||||
|
<Radio.Indicator className={style.indicator} />
|
||||||
|
</Radio.Root>
|
||||||
|
{item.label}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</BaseRadioGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,22 +1,12 @@
|
|||||||
$input-font-size: 15px;
|
|
||||||
|
|
||||||
.delayInput {
|
.delayInput {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: $element-spacing;
|
gap: $element-spacing;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: $text-body-size;
|
|
||||||
|
|
||||||
|
|
||||||
.inputField {
|
|
||||||
font-size: $input-font-size;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
max-width: 7em;
|
|
||||||
padding-left: 16px;
|
|
||||||
color: $ontime-delay-text
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.delayOptions {
|
.inputField {
|
||||||
display: flex;
|
text-align: center;
|
||||||
flex-direction: column;
|
letter-spacing: 1px;
|
||||||
|
max-width: 7em;
|
||||||
|
color: $ontime-delay-text
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { KeyboardEvent, useEffect, useRef, useState } from 'react';
|
import { KeyboardEvent, useEffect, useRef, useState } from 'react';
|
||||||
import { Input, Radio, RadioGroup } from '@chakra-ui/react';
|
|
||||||
import { millisToString, parseUserTime } from 'ontime-utils';
|
import { millisToString, parseUserTime } from 'ontime-utils';
|
||||||
|
|
||||||
import { useEntryActions } from '../../../hooks/useEntryAction';
|
import { useEntryActions } from '../../../hooks/useEntryAction';
|
||||||
|
import Input from '../input/Input';
|
||||||
|
|
||||||
|
import BlockRadio from './BlockRadio';
|
||||||
|
|
||||||
import style from './DelayInput.module.scss';
|
import style from './DelayInput.module.scss';
|
||||||
|
|
||||||
@@ -105,13 +107,10 @@ export default function DelayInput(props: DelayInputProps) {
|
|||||||
return (
|
return (
|
||||||
<div className={style.delayInput}>
|
<div className={style.delayInput}>
|
||||||
<Input
|
<Input
|
||||||
size='sm'
|
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
data-testid='delay-input'
|
data-testid='delay-input'
|
||||||
className={style.inputField}
|
className={style.inputField}
|
||||||
type='text'
|
|
||||||
placeholder='-'
|
placeholder='-'
|
||||||
variant='ontime-filled'
|
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onChange={(event) => setValue(event.target.value)}
|
onChange={(event) => setValue(event.target.value)}
|
||||||
onBlur={(event) => validateAndSubmit(event.target.value)}
|
onBlur={(event) => validateAndSubmit(event.target.value)}
|
||||||
@@ -119,16 +118,14 @@ export default function DelayInput(props: DelayInputProps) {
|
|||||||
value={value}
|
value={value}
|
||||||
maxLength={9}
|
maxLength={9}
|
||||||
/>
|
/>
|
||||||
<RadioGroup
|
<BlockRadio
|
||||||
className={style.delayOptions}
|
onValueChange={handleSlipChange}
|
||||||
onChange={handleSlipChange}
|
|
||||||
value={checkedOption}
|
value={checkedOption}
|
||||||
variant='ontime-block'
|
items={[
|
||||||
size='sm'
|
{ value: 'add', label: 'Add time' },
|
||||||
>
|
{ value: 'subtract', label: 'Subtract time' },
|
||||||
<Radio value='add'>Add time</Radio>
|
]}
|
||||||
<Radio value='subtract'>Subtract time</Radio>
|
/>
|
||||||
</RadioGroup>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,36 @@
|
|||||||
|
// styles from Input.module.scss
|
||||||
.input {
|
.input {
|
||||||
color: $gray-200;
|
width: 100%;
|
||||||
border: 1px solid transparent;
|
margin-top: 0.25rem;
|
||||||
border-radius: 0 0 8px 8px;
|
box-sizing: border-box;
|
||||||
background-color: $gray-1200;
|
|
||||||
padding: 0 0.5rem;
|
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: $gray-200;
|
||||||
|
border-radius: $component-border-radius-md;
|
||||||
|
background-color: $gray-1200;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
|
padding-inline: 0.5em;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover:not(:disabled) {
|
||||||
background-color: $gray-1100;
|
background-color: $gray-1100;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus:not(:read-only) {
|
||||||
background-color: $gray-1000;
|
background-color: $gray-1000;
|
||||||
color: $gray-50;
|
|
||||||
border: 1px solid $blue-500;
|
border: 1px solid $blue-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: $gray-500;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
import { HexAlphaColorPicker, HexColorInput } from 'react-colorful';
|
import { HexAlphaColorPicker, HexColorInput } from 'react-colorful';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@chakra-ui/react';
|
import { Popover } from '@base-ui-components/react/popover';
|
||||||
|
|
||||||
|
import PopoverContents from '../../popover/Popover';
|
||||||
|
|
||||||
import style from './PopoverPicker.module.scss';
|
import style from './PopoverPicker.module.scss';
|
||||||
|
|
||||||
@@ -9,15 +11,14 @@ interface PopoverPickerProps {
|
|||||||
onChange: (color: string) => void;
|
onChange: (color: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PopoverPicker(props: PropsWithChildren<PopoverPickerProps>) {
|
export default function PopoverPicker({ color, onChange, children }: PropsWithChildren<PopoverPickerProps>) {
|
||||||
const { color, onChange, children } = props;
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover.Root>
|
||||||
<PopoverTrigger>{children}</PopoverTrigger>
|
<Popover.Trigger>{children}</Popover.Trigger>
|
||||||
<PopoverContent className={style.small} style={{ borderRadius: '9px', width: 'auto' }}>
|
<PopoverContents>
|
||||||
<HexAlphaColorPicker color={color} onChange={onChange} />
|
<HexAlphaColorPicker color={color} onChange={onChange} />
|
||||||
<HexColorInput color={color} onChange={onChange} className={style.input} prefixed />
|
<HexColorInput color={color} onChange={onChange} className={style.input} prefixed />
|
||||||
</PopoverContent>
|
</PopoverContents>
|
||||||
</Popover>
|
</Popover.Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ interface UseReactiveTextInputReturn {
|
|||||||
export default function useReactiveTextInput(
|
export default function useReactiveTextInput(
|
||||||
initialText: string,
|
initialText: string,
|
||||||
submitCallback: (newValue: string) => void,
|
submitCallback: (newValue: string) => void,
|
||||||
ref: RefObject<HTMLInputElement>,
|
ref: RefObject<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
options?: {
|
options?: {
|
||||||
submitOnEnter?: boolean;
|
submitOnEnter?: boolean;
|
||||||
submitOnCtrlEnter?: boolean;
|
submitOnCtrlEnter?: boolean;
|
||||||
@@ -101,25 +101,31 @@ export default function useReactiveTextInput(
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (options?.submitOnEnter) {
|
if (options?.submitOnEnter) {
|
||||||
hotKeys.push(['Enter', () => {
|
hotKeys.push([
|
||||||
isKeyboardSubmitting.current = true;
|
'Enter',
|
||||||
handleSubmit(text);
|
() => {
|
||||||
// clear flag after blur has been processed
|
isKeyboardSubmitting.current = true;
|
||||||
setTimeout(() => {
|
handleSubmit(text);
|
||||||
isKeyboardSubmitting.current = false;
|
// clear flag after blur has been processed
|
||||||
}, 0);
|
setTimeout(() => {
|
||||||
}]);
|
isKeyboardSubmitting.current = false;
|
||||||
|
}, 0);
|
||||||
|
},
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.submitOnCtrlEnter) {
|
if (options?.submitOnCtrlEnter) {
|
||||||
hotKeys.push(['mod + Enter', () => {
|
hotKeys.push([
|
||||||
isKeyboardSubmitting.current = true;
|
'mod + Enter',
|
||||||
handleSubmit(text);
|
() => {
|
||||||
// clear flag after blur has been processed
|
isKeyboardSubmitting.current = true;
|
||||||
setTimeout(() => {
|
handleSubmit(text);
|
||||||
isKeyboardSubmitting.current = false;
|
// clear flag after blur has been processed
|
||||||
}, 0);
|
setTimeout(() => {
|
||||||
}]);
|
isKeyboardSubmitting.current = false;
|
||||||
|
}, 0);
|
||||||
|
},
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hotKeyHandler = getHotkeyHandler(hotKeys);
|
const hotKeyHandler = getHotkeyHandler(hotKeys);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
@use '../../../../theme/viewerDefs' as *;
|
|
||||||
|
|
||||||
.textarea {
|
.textarea {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
min-height: 2rem;
|
||||||
|
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
.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);
|
||||||
|
|
||||||
@@ -21,7 +20,6 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: $zindex-dialog;
|
|
||||||
|
|
||||||
width: 22rem;
|
width: 22rem;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
|||||||
+1
-6
@@ -30,12 +30,7 @@ export default function OtherAddresses({ currentLocation }: OtherAddressesProps)
|
|||||||
const address = linkToOtherHost(nif.address, currentLocation);
|
const address = linkToOtherHost(nif.address, currentLocation);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CopyTag
|
<CopyTag key={nif.name} copyValue={address} onClick={() => openLink(address)}>
|
||||||
key={nif.name}
|
|
||||||
copyValue={address}
|
|
||||||
onClick={() => openLink(address)}
|
|
||||||
label='Copy IP or navigate to address'
|
|
||||||
>
|
|
||||||
{nif.address} <IoArrowUp className={style.goIcon} />
|
{nif.address} <IoArrowUp className={style.goIcon} />
|
||||||
</CopyTag>
|
</CopyTag>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
padding-top: 25vh;
|
||||||
|
|
||||||
|
background: $ui-black;
|
||||||
|
color: $ontime-color;
|
||||||
|
font-size: 3rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-size: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
width: 4em;
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: 0.25em;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
height: 4rem;
|
||||||
|
width: 4rem;
|
||||||
|
font-size: 4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pinFailed {
|
||||||
|
input {
|
||||||
|
animation: redFlash 1.5s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes redFlash {
|
||||||
|
from {
|
||||||
|
background: $red-500;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
background: rgba($red-500, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,71 +1,55 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { PropsWithChildren, useState } from 'react';
|
||||||
import { IoCheckmark } from 'react-icons/io5';
|
import { IoCheckmark } from 'react-icons/io5';
|
||||||
import { IconButton, PinInput, PinInputField } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
import style from './ProtectRoute.module.scss';
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
import IconButton from '../buttons/IconButton';
|
||||||
|
import Input from '../input/input/Input';
|
||||||
|
|
||||||
|
import style from './PinPage.module.scss';
|
||||||
|
|
||||||
interface PinPageProps {
|
interface PinPageProps {
|
||||||
permission: 'editor' | 'operator';
|
permission: 'editor' | 'operator';
|
||||||
handleValidation: (pin: string) => boolean;
|
handleValidation: (pin: string) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PinPage(props: PinPageProps) {
|
export default function PinPage({ permission, handleValidation }: PropsWithChildren<PinPageProps>) {
|
||||||
const { permission, handleValidation } = props;
|
|
||||||
const [pin, setPin] = useState('');
|
const [pin, setPin] = useState('');
|
||||||
const [failed, setFailed] = useState(false);
|
const [failed, setFailed] = useState(false);
|
||||||
|
|
||||||
const validate = useCallback(() => {
|
const validate = () => {
|
||||||
const isValid = handleValidation(pin);
|
const isValid = handleValidation(pin);
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
setFailed(true);
|
setFailed(true);
|
||||||
setPin('');
|
setPin('');
|
||||||
}
|
}
|
||||||
}, [handleValidation, pin]);
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const handleInputChange = (value: string) => {
|
||||||
const handleKeyPress = (event: KeyboardEvent) => {
|
setPin(value);
|
||||||
if (event.repeat) return;
|
if (failed) setFailed(false);
|
||||||
if (event.key === 'Enter') {
|
};
|
||||||
validate();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('keydown', handleKeyPress);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('keydown', handleKeyPress);
|
|
||||||
};
|
|
||||||
}, [validate]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.container}>
|
<div className={style.container}>
|
||||||
{`Ontime ${permission}`}
|
{`Ontime ${permission}`}
|
||||||
<div className={failed ? style.pin__failed : style.pin}>
|
<form
|
||||||
<PinInput
|
onSubmit={(event) => {
|
||||||
type='alphanumeric'
|
event.preventDefault();
|
||||||
size='lg'
|
validate();
|
||||||
mask
|
}}
|
||||||
autoFocus
|
className={cx([style.pin, failed && style.pinFailed])}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type='password'
|
||||||
|
maxLength={4}
|
||||||
|
height='large'
|
||||||
value={pin}
|
value={pin}
|
||||||
onChange={(value) => {
|
onChange={(e) => handleInputChange(e.target.value)}
|
||||||
setFailed(false);
|
|
||||||
setPin(value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PinInputField />
|
|
||||||
<PinInputField />
|
|
||||||
<PinInputField />
|
|
||||||
<PinInputField />
|
|
||||||
</PinInput>
|
|
||||||
<IconButton
|
|
||||||
variant='ontime-filled'
|
|
||||||
aria-label='Enter'
|
|
||||||
size='lg'
|
|
||||||
isRound
|
|
||||||
icon={<IoCheckmark />}
|
|
||||||
onClick={validate}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
<IconButton type='submit' variant='primary' aria-label='Enter'>
|
||||||
|
<IoCheckmark />
|
||||||
|
</IconButton>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
.container {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding-top: 25vh;
|
|
||||||
|
|
||||||
background: $ui-black;
|
|
||||||
color: $ontime-color;
|
|
||||||
font-weight: 200;
|
|
||||||
font-size: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pin,
|
|
||||||
.pin__failed {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.125em;
|
|
||||||
padding-block: 0.5em;
|
|
||||||
|
|
||||||
input {
|
|
||||||
border-radius: 99px;
|
|
||||||
border-color: $gray-500;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: $blue-500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
margin-left: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pin__failed {
|
|
||||||
input {
|
|
||||||
animation: colourFade 1.5s ease;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes colourFade {
|
|
||||||
from {
|
|
||||||
background: $red-500;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
background: rgba($red-500, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
.radioGroup {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
color: $gray-900;
|
||||||
|
font-size: calc(1rem - 2px);
|
||||||
|
color: $ui-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal {
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical {
|
||||||
|
align-items: start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
&:has([data-checked]) {
|
||||||
|
color: $ui-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 100%;
|
||||||
|
outline: 0;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&[data-unchecked] {
|
||||||
|
border: 1px solid $gray-1200;
|
||||||
|
background-color: $gray-1200;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-checked] {
|
||||||
|
background-color: $ui-white;
|
||||||
|
&:hover {
|
||||||
|
border-color: $gray-1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid $blue-500;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
|
||||||
|
&[data-unchecked] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
border-radius: 100%;
|
||||||
|
width: 0.5rem;
|
||||||
|
height: 0.5rem;
|
||||||
|
background-color: $gray-1200;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Radio } from '@base-ui-components/react/radio';
|
||||||
|
import { RadioGroup as BaseRadioGroup } from '@base-ui-components/react/radio-group';
|
||||||
|
|
||||||
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|
||||||
|
import style from './RadioGroup.module.scss';
|
||||||
|
|
||||||
|
interface RadioGroupProps<T extends string | number | boolean> extends Omit<BaseRadioGroup.Props, 'onValueChange'> {
|
||||||
|
items: {
|
||||||
|
value: T;
|
||||||
|
label: string;
|
||||||
|
}[];
|
||||||
|
onValueChange?: (value: T) => void;
|
||||||
|
orientation?: 'horizontal' | 'vertical';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RadioGroup<T extends string | number | boolean>({
|
||||||
|
items,
|
||||||
|
className,
|
||||||
|
orientation = 'vertical',
|
||||||
|
onValueChange,
|
||||||
|
...elementProps
|
||||||
|
}: RadioGroupProps<T>) {
|
||||||
|
return (
|
||||||
|
<BaseRadioGroup
|
||||||
|
onValueChange={(value) => onValueChange?.(value as T)}
|
||||||
|
className={cx([style.radioGroup, style[orientation], className])}
|
||||||
|
{...elementProps}
|
||||||
|
>
|
||||||
|
{items.map((item) => (
|
||||||
|
<label className={style.item} key={item.value.toString()}>
|
||||||
|
<Radio.Root value={item.value.toString()} className={style.radio}>
|
||||||
|
<Radio.Indicator className={style.indicator} />
|
||||||
|
</Radio.Root>
|
||||||
|
{item.label}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</BaseRadioGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -38,10 +38,18 @@
|
|||||||
outline: 2px solid $blue-500;
|
outline: 2px solid $blue-500;
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.fluid {
|
.medium {
|
||||||
width: 100%;
|
height: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.large {
|
||||||
|
height: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fluid {
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selectIcon {
|
.selectIcon {
|
||||||
|
|||||||
@@ -6,20 +6,22 @@ import { cx } from '../../utils/styleUtils';
|
|||||||
|
|
||||||
import styles from './Select.module.scss';
|
import styles from './Select.module.scss';
|
||||||
|
|
||||||
|
export type SelectOption<T = string> = {
|
||||||
|
value: T;
|
||||||
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
interface SelectProps<T> extends Omit<BaseSelect.Root.Props<T>, 'items'> {
|
interface SelectProps<T> extends Omit<BaseSelect.Root.Props<T>, 'items'> {
|
||||||
// overload items to not allow undefined values
|
options: SelectOption<T>[];
|
||||||
options: {
|
|
||||||
value: T;
|
|
||||||
label: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
}[];
|
|
||||||
fluid?: boolean;
|
fluid?: boolean;
|
||||||
|
size?: 'medium' | 'large';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Select<T>({ options, fluid, ...selectRootProps }: SelectProps<T>) {
|
export default function Select<T>({ options, fluid, size = 'medium', ...selectRootProps }: SelectProps<T>) {
|
||||||
return (
|
return (
|
||||||
<BaseSelect.Root items={options} {...selectRootProps}>
|
<BaseSelect.Root items={options} {...selectRootProps}>
|
||||||
<BaseSelect.Trigger className={cx([styles.select, fluid && styles.fluid])}>
|
<BaseSelect.Trigger className={cx([styles.select, styles[size], fluid && styles.fluid])}>
|
||||||
<BaseSelect.Value />
|
<BaseSelect.Value />
|
||||||
<BaseSelect.Icon className={styles.selectIcon}>
|
<BaseSelect.Icon className={styles.selectIcon}>
|
||||||
<LuChevronsUpDown />
|
<LuChevronsUpDown />
|
||||||
|
|||||||
@@ -19,7 +19,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
border-color: $blue-500;
|
outline: 2px solid $blue-500;
|
||||||
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
.inline {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// styles from subtle button
|
||||||
|
.toggleSelect {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding-inline: 0.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
background: $gray-1050;
|
||||||
|
color: $ui-white;
|
||||||
|
line-height: 1em;
|
||||||
|
border-radius: $component-border-radius-md;
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:active) {
|
||||||
|
background: $gray-1000;
|
||||||
|
color: $blue-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: $gray-1100;
|
||||||
|
border-color: $gray-1250;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
width: 0.75em;
|
||||||
|
height: 0.75em;
|
||||||
|
background: var(--user-bg);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
color: $ui-white;
|
||||||
|
}
|
||||||
@@ -1,25 +1,17 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { IoChevronDown } from 'react-icons/io5';
|
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Input,
|
|
||||||
InputGroup,
|
|
||||||
InputLeftElement,
|
|
||||||
Menu,
|
|
||||||
MenuButton,
|
|
||||||
MenuItemOption,
|
|
||||||
MenuList,
|
|
||||||
MenuOptionGroup,
|
|
||||||
Select,
|
|
||||||
Switch,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
|
|
||||||
import { isStringBoolean } from '../../../features/viewers/common/viewUtils';
|
import { isStringBoolean } from '../../../features/viewers/common/viewUtils';
|
||||||
|
import Checkbox from '../checkbox/Checkbox';
|
||||||
|
import Input from '../input/input/Input';
|
||||||
|
import Select from '../select/Select';
|
||||||
|
import Switch from '../switch/Switch';
|
||||||
|
|
||||||
import InlineColourPicker from './InlineColourPicker';
|
import InlineColourPicker from './InlineColourPicker';
|
||||||
import { ParamField } from './viewParams.types';
|
import { ParamField } from './viewParams.types';
|
||||||
|
|
||||||
|
import style from './ParamInput.module.scss';
|
||||||
|
|
||||||
interface ParamInputProps {
|
interface ParamInputProps {
|
||||||
paramField: ParamField;
|
paramField: ParamField;
|
||||||
}
|
}
|
||||||
@@ -39,20 +31,11 @@ export default function ParamInput({ paramField }: ParamInputProps) {
|
|||||||
const optionFromParams = searchParams.get(id);
|
const optionFromParams = searchParams.get(id);
|
||||||
const defaultOptionValue = optionFromParams || defaultValue;
|
const defaultOptionValue = optionFromParams || defaultValue;
|
||||||
|
|
||||||
return (
|
if (paramField.values.length === 0) {
|
||||||
<Select
|
return <span className={style.empty}>No options available</span>;
|
||||||
placeholder={defaultValue ? undefined : 'Select an option'}
|
}
|
||||||
variant='ontime'
|
|
||||||
name={id}
|
return <Select size='large' name={id} defaultValue={defaultOptionValue} options={paramField.values} />;
|
||||||
defaultValue={defaultOptionValue}
|
|
||||||
>
|
|
||||||
{Object.entries(paramField.values).map(([key, value]) => (
|
|
||||||
<option key={key} value={key}>
|
|
||||||
{value}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'multi-option') {
|
if (type === 'multi-option') {
|
||||||
@@ -61,27 +44,22 @@ export default function ParamInput({ paramField }: ParamInputProps) {
|
|||||||
|
|
||||||
if (type === 'boolean') {
|
if (type === 'boolean') {
|
||||||
const defaultCheckedValue = isStringBoolean(searchParams.get(id)) || defaultValue;
|
const defaultCheckedValue = isStringBoolean(searchParams.get(id)) || defaultValue;
|
||||||
|
return <Switch size='large' name={id} defaultChecked={defaultCheckedValue} />;
|
||||||
// checked value should be 'true', so it can be captured by the form event
|
|
||||||
return <Switch variant='ontime' name={id} defaultChecked={defaultCheckedValue} value='true' />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'number') {
|
if (type === 'number') {
|
||||||
const { prefix, placeholder } = paramField;
|
const { placeholder } = paramField;
|
||||||
const defaultNumberValue = searchParams.get(id) ?? defaultValue;
|
const defaultNumberValue = searchParams.get(id) ?? defaultValue;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputGroup variant='ontime-filled'>
|
<Input
|
||||||
{prefix && <InputLeftElement pointerEvents='none'>{prefix}</InputLeftElement>}
|
height='large'
|
||||||
<Input
|
type='number'
|
||||||
type='number'
|
step='any'
|
||||||
step='any'
|
name={id}
|
||||||
variant='ontime-filled'
|
defaultValue={defaultNumberValue}
|
||||||
name={id}
|
placeholder={placeholder}
|
||||||
defaultValue={defaultNumberValue}
|
/>
|
||||||
placeholder={placeholder}
|
|
||||||
/>
|
|
||||||
</InputGroup>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,52 +70,56 @@ export default function ParamInput({ paramField }: ParamInputProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaultStringValue = searchParams.get(id) ?? defaultValue;
|
const defaultStringValue = searchParams.get(id) ?? defaultValue;
|
||||||
const { prefix, placeholder } = paramField;
|
const { placeholder } = paramField;
|
||||||
|
|
||||||
return (
|
return <Input height='large' name={id} defaultValue={defaultStringValue} placeholder={placeholder} />;
|
||||||
<InputGroup variant='ontime-filled'>
|
|
||||||
{prefix && <InputLeftElement pointerEvents='none'>{prefix}</InputLeftElement>}
|
|
||||||
<Input name={id} defaultValue={defaultStringValue} placeholder={placeholder} />
|
|
||||||
</InputGroup>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EditFormMultiOptionProps {
|
interface EditFormMultiOptionProps {
|
||||||
paramField: ParamField & { type: 'multi-option' };
|
paramField: ParamField & { type: 'multi-option' };
|
||||||
}
|
}
|
||||||
|
|
||||||
function MultiOption(props: EditFormMultiOptionProps) {
|
function MultiOption({ paramField }: EditFormMultiOptionProps) {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const { paramField } = props;
|
const { id, values, defaultValue = [''] } = paramField;
|
||||||
const { id, defaultValue } = paramField;
|
|
||||||
|
|
||||||
const optionFromParams = searchParams.getAll(id);
|
const optionFromParams = searchParams.getAll(id);
|
||||||
const [paramState, setParamState] = useState<string[]>(optionFromParams || defaultValue || ['']);
|
const [paramState, setParamState] = useState<string[]>(optionFromParams || defaultValue);
|
||||||
|
|
||||||
|
const toggleValue = (value: string, checked: boolean) => {
|
||||||
|
if (checked) {
|
||||||
|
setParamState((prev) => [...prev, value]);
|
||||||
|
} else {
|
||||||
|
setParamState((prev) => prev.filter((v) => v !== value));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (values.length === 0) {
|
||||||
|
return <span className={style.empty}>No options available</span>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<input name={id} hidden readOnly value={paramState} />
|
<input name={id} hidden readOnly value={paramState.join(',')} />
|
||||||
<Menu isLazy closeOnSelect={false} variant='ontime-on-dark'>
|
<div className={style.inline}>
|
||||||
<MenuButton as={Button} variant='ontime-subtle-white' position='relative' width='fit-content' fontWeight={400}>
|
{values.map((option) => {
|
||||||
{paramField.title} <IoChevronDown style={{ display: 'inline' }} />
|
return (
|
||||||
</MenuButton>
|
<label
|
||||||
<MenuList overflow='auto' maxHeight='200px'>
|
key={option.value}
|
||||||
<MenuOptionGroup
|
className={style.toggleSelect}
|
||||||
type='checkbox'
|
style={{
|
||||||
value={paramState}
|
'--user-bg': option.colour,
|
||||||
onChange={(value) => setParamState(Array.isArray(value) ? value : [value])}
|
}}
|
||||||
>
|
>
|
||||||
{Object.values(paramField.values).map((option) => {
|
<Checkbox
|
||||||
const { value, label, colour } = option;
|
checked={paramState.includes(option.value)}
|
||||||
return (
|
onCheckedChange={(checked) => toggleValue(option.value, checked as boolean)}
|
||||||
<MenuItemOption value={value} key={value} style={{ borderRight: `8px solid ${colour}` }}>
|
/>
|
||||||
{label}
|
{option.label}
|
||||||
</MenuItemOption>
|
</label>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</MenuOptionGroup>
|
</div>
|
||||||
</MenuList>
|
|
||||||
</Menu>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
.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);
|
||||||
|
|
||||||
@@ -27,14 +26,13 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: $zindex-dialog;
|
|
||||||
|
|
||||||
width: 40rem;
|
width: 40rem;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 1rem 1.5rem;
|
padding-block: 1rem 1.5rem;
|
||||||
|
|
||||||
background-color: $gray-1250;
|
background-color: $gray-1250;
|
||||||
color: $ui-white;
|
color: $ui-white;
|
||||||
@@ -58,6 +56,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
padding-inline: 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -69,12 +68,14 @@
|
|||||||
|
|
||||||
.body {
|
.body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
padding-inline: 1rem;
|
||||||
padding-bottom: 10vh;
|
padding-bottom: 10vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
padding-inline: 1rem;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { FormEvent, memo } from 'react';
|
import { FormEvent, memo } from 'react';
|
||||||
import { IoClose } from 'react-icons/io5';
|
import { IoClose } from 'react-icons/io5';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { Dialog } from '@base-ui-components/react/dialog';
|
import { Dialog } from '@base-ui-components/react/dialog';
|
||||||
|
|
||||||
import useViewSettings from '../../hooks-query/useViewSettings';
|
import useViewSettings from '../../hooks-query/useViewSettings';
|
||||||
@@ -21,6 +22,7 @@ interface EditFormDrawerProps {
|
|||||||
export default memo(ViewParamsEditor);
|
export default memo(ViewParamsEditor);
|
||||||
|
|
||||||
function ViewParamsEditor({ viewOptions }: EditFormDrawerProps) {
|
function ViewParamsEditor({ viewOptions }: EditFormDrawerProps) {
|
||||||
|
const [_, setSearchParams] = useSearchParams();
|
||||||
const { data: viewSettings } = useViewSettings();
|
const { data: viewSettings } = useViewSettings();
|
||||||
const { isOpen, close } = useViewParamsEditorStore();
|
const { isOpen, close } = useViewParamsEditorStore();
|
||||||
|
|
||||||
@@ -29,8 +31,7 @@ function ViewParamsEditor({ viewOptions }: EditFormDrawerProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const resetParams = () => {
|
const resetParams = () => {
|
||||||
window.history.pushState(null, '', window.location.pathname);
|
setSearchParams();
|
||||||
close();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onParamsFormSubmit = (formEvent: FormEvent<HTMLFormElement>) => {
|
const onParamsFormSubmit = (formEvent: FormEvent<HTMLFormElement>) => {
|
||||||
@@ -38,9 +39,9 @@ function ViewParamsEditor({ viewOptions }: EditFormDrawerProps) {
|
|||||||
|
|
||||||
const newParamsObject = Object.fromEntries(new FormData(formEvent.currentTarget));
|
const newParamsObject = Object.fromEntries(new FormData(formEvent.currentTarget));
|
||||||
const newSearchParams = getURLSearchParamsFromObj(newParamsObject, viewOptions);
|
const newSearchParams = getURLSearchParamsFromObj(newParamsObject, viewOptions);
|
||||||
const url = new URL(window.location.href);
|
|
||||||
url.search = newSearchParams.toString();
|
console.log('New search params:', newParamsObject, newSearchParams.toString());
|
||||||
window.history.pushState(null, '', url);
|
setSearchParams(newSearchParams);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+57
-22
@@ -5,32 +5,32 @@ import { OptionTitle } from '../constants';
|
|||||||
import type { ViewOption } from '../viewParams.types';
|
import type { ViewOption } from '../viewParams.types';
|
||||||
import { getURLSearchParamsFromObj, makeOptionsFromCustomFields } from '../viewParams.utils';
|
import { getURLSearchParamsFromObj, makeOptionsFromCustomFields } from '../viewParams.utils';
|
||||||
|
|
||||||
describe('makeOptionsFromCustomFields', () => {
|
describe('makeOptionsFromCustomFields()', () => {
|
||||||
const testCustomFields: CustomFields = {
|
const testCustomFields: CustomFields = {
|
||||||
field1: { label: 'Field 1', colour: 'red', type: 'string' },
|
field1: { label: 'Field 1', colour: 'red', type: 'string' },
|
||||||
field2: { label: 'Field 2', colour: 'blue', type: 'string' },
|
field2: { label: 'Field 2', colour: 'blue', type: 'string' },
|
||||||
};
|
};
|
||||||
|
|
||||||
it('creates a record of keys for the given custom fields', () => {
|
it('creates an array of options to use in a select', () => {
|
||||||
const result = makeOptionsFromCustomFields(testCustomFields);
|
const result = makeOptionsFromCustomFields(testCustomFields);
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual([
|
||||||
'custom-field1': 'Custom: Field 1',
|
{ value: 'custom-field1', label: 'Custom: Field 1' },
|
||||||
'custom-field2': 'Custom: Field 2',
|
{ value: 'custom-field2', label: 'Custom: Field 2' },
|
||||||
});
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('appends additional data', () => {
|
it('appends additional data', () => {
|
||||||
const additionalData = {
|
const additionalData = [
|
||||||
test1: 'test1',
|
{ value: 'test1', label: 'Test 1' },
|
||||||
test2: 'test2',
|
{ value: 'test2', label: 'Test 2' },
|
||||||
};
|
];
|
||||||
const result = makeOptionsFromCustomFields(testCustomFields, additionalData);
|
const result = makeOptionsFromCustomFields(testCustomFields, additionalData);
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual([
|
||||||
'custom-field1': 'Custom: Field 1',
|
{ value: 'custom-field1', label: 'Custom: Field 1' },
|
||||||
'custom-field2': 'Custom: Field 2',
|
{ value: 'custom-field2', label: 'Custom: Field 2' },
|
||||||
test1: 'test1',
|
{ value: 'test1', label: 'Test 1' },
|
||||||
test2: 'test2',
|
{ value: 'test2', label: 'Test 2' },
|
||||||
});
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('filtersImageTypes', () => {
|
it('filtersImageTypes', () => {
|
||||||
@@ -40,14 +40,14 @@ describe('makeOptionsFromCustomFields', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const result = makeOptionsFromCustomFields(customFieldsWIthImage);
|
const result = makeOptionsFromCustomFields(customFieldsWIthImage);
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual([
|
||||||
'custom-field1': 'Custom: Field 1',
|
{ value: 'custom-field1', label: 'Custom: Field 1' },
|
||||||
'custom-field2': 'Custom: Field 2',
|
{ value: 'custom-field2', label: 'Custom: Field 2' },
|
||||||
});
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getURLSearchParamsFromObj', () => {
|
describe('getURLSearchParamsFromObj()', () => {
|
||||||
// Mock view options for testing
|
// Mock view options for testing
|
||||||
const mockViewOptions: ViewOption[] = [
|
const mockViewOptions: ViewOption[] = [
|
||||||
{
|
{
|
||||||
@@ -73,7 +73,10 @@ describe('getURLSearchParamsFromObj', () => {
|
|||||||
title: 'Multi Select',
|
title: 'Multi Select',
|
||||||
description: 'A multi-select field',
|
description: 'A multi-select field',
|
||||||
type: 'option',
|
type: 'option',
|
||||||
values: { value1: 'Value 1', value2: 'Value 2' },
|
values: [
|
||||||
|
{ value: 'value1', label: 'Value 1' },
|
||||||
|
{ value: 'value2', label: 'Value 2' },
|
||||||
|
],
|
||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -201,4 +204,36 @@ describe('getURLSearchParamsFromObj', () => {
|
|||||||
// Should only include unique values while maintaining order
|
// Should only include unique values while maintaining order
|
||||||
expect(result.getAll('sub')).toEqual(['value1', 'value2', 'value3']);
|
expect(result.getAll('sub')).toEqual(['value1', 'value2', 'value3']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('converts on-off from toggle to boolean', () => {
|
||||||
|
const mockOptionsWithBooleans: ViewOption[] = [
|
||||||
|
{
|
||||||
|
title: OptionTitle.StyleOverride,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
id: 'bool1',
|
||||||
|
title: 'bool1',
|
||||||
|
description: 'Bool1',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bool2',
|
||||||
|
title: 'bool2',
|
||||||
|
description: 'Bool2',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const params = {
|
||||||
|
bool1: 'off',
|
||||||
|
bool2: 'on',
|
||||||
|
};
|
||||||
|
const result = getURLSearchParamsFromObj(params, mockOptionsWithBooleans);
|
||||||
|
console.log('Result:', result.toString());
|
||||||
|
expect(result.get('bool1')).toBe('false');
|
||||||
|
expect(result.get('bool2')).toBe('true');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,20 +8,19 @@ type BaseField = {
|
|||||||
|
|
||||||
type OptionsField = {
|
type OptionsField = {
|
||||||
type: 'option';
|
type: 'option';
|
||||||
values: Record<string, string>;
|
values: { value: string; label: string }[];
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MultiselectOption = { value: string; label: string; colour: string };
|
export type MultiselectOption = { value: string; label: string; colour: string };
|
||||||
export type MultiselectOptions = Record<string, MultiselectOption>;
|
|
||||||
type MultiOptionsField = {
|
type MultiOptionsField = {
|
||||||
type: 'multi-option';
|
type: 'multi-option';
|
||||||
values: MultiselectOptions;
|
values: MultiselectOption[];
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StringField = { type: 'string'; defaultValue?: string; prefix?: string; placeholder?: string };
|
type StringField = { type: 'string'; defaultValue?: string; placeholder?: string };
|
||||||
type NumberField = { type: 'number'; defaultValue?: number; prefix?: string; placeholder?: string };
|
type NumberField = { type: 'number'; defaultValue?: number; placeholder?: string };
|
||||||
type BooleanField = { type: 'boolean'; defaultValue: boolean };
|
type BooleanField = { type: 'boolean'; defaultValue: boolean };
|
||||||
type ColourField = { type: 'colour'; defaultValue: string; placeholder?: string };
|
type ColourField = { type: 'colour'; defaultValue: string; placeholder?: string };
|
||||||
type PersistedField = { type: 'persist'; defaultValue?: string[]; values: string[] };
|
type PersistedField = { type: 'persist'; defaultValue?: string[]; values: string[] };
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { CustomFields } from 'ontime-types';
|
import type { CustomFields } from 'ontime-types';
|
||||||
|
|
||||||
import type { MultiselectOptions, ViewOption } from './viewParams.types';
|
import type { SelectOption } from '../select/Select';
|
||||||
|
|
||||||
|
import type { MultiselectOption, ViewOption } from './viewParams.types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a list of custom fields for a select
|
* Creates a list of custom fields for a select
|
||||||
@@ -8,32 +10,46 @@ import type { MultiselectOptions, ViewOption } from './viewParams.types';
|
|||||||
*/
|
*/
|
||||||
export function makeOptionsFromCustomFields(
|
export function makeOptionsFromCustomFields(
|
||||||
customFields: CustomFields,
|
customFields: CustomFields,
|
||||||
additionalOptions: Readonly<Record<string, string>> = {},
|
additionalOptions: SelectOption[] = [],
|
||||||
filterImageType = true,
|
filterImageType = true,
|
||||||
): Record<string, string> {
|
): SelectOption[] {
|
||||||
const options = { ...additionalOptions };
|
const options: SelectOption[] = [];
|
||||||
|
|
||||||
|
// Add custom fields first
|
||||||
for (const [key, value] of Object.entries(customFields)) {
|
for (const [key, value] of Object.entries(customFields)) {
|
||||||
if (filterImageType && value.type === 'image') {
|
if (filterImageType && value.type === 'image') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
options[`custom-${key}`] = `Custom: ${value.label}`;
|
options.push({
|
||||||
|
value: `custom-${key}`,
|
||||||
|
label: `Custom: ${value.label}`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return options;
|
|
||||||
|
return options.concat(additionalOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates data for a multiselect component from custom fields
|
* Creates data for a multiselect component from custom fields
|
||||||
* Filters out image type custom fields
|
* Filters out image type custom fields
|
||||||
*/
|
*/
|
||||||
export function makeCustomFieldSelectOptions(customFields: CustomFields, filterImageType = true): MultiselectOptions {
|
export function makeCustomFieldSelectOptions(customFields: CustomFields, filterImageType = true): MultiselectOption[] {
|
||||||
const options: MultiselectOptions = {};
|
const options: MultiselectOption[] = [];
|
||||||
|
|
||||||
|
// Add custom fields first
|
||||||
for (const [key, value] of Object.entries(customFields)) {
|
for (const [key, value] of Object.entries(customFields)) {
|
||||||
if (filterImageType && value.type === 'image') {
|
if (filterImageType && value.type === 'image') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
options[key] = { value: key, label: value.label, colour: value.colour };
|
|
||||||
|
options.push({
|
||||||
|
value: key,
|
||||||
|
label: value.label,
|
||||||
|
colour: value.colour || 'transparent',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,17 +68,22 @@ function sanitiseColour(colour: string) {
|
|||||||
type FieldMetadata = {
|
type FieldMetadata = {
|
||||||
defaultValues: Record<string, string>;
|
defaultValues: Record<string, string>;
|
||||||
colorFields: Set<string>;
|
colorFields: Set<string>;
|
||||||
|
booleanFields: Set<string>;
|
||||||
isPersistedField: Set<string>;
|
isPersistedField: Set<string>;
|
||||||
persistedValues: Record<string, string[]>;
|
persistedValues: Record<string, string[]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility collects metadata about fields from view options
|
* Utility collects metadata about fields from view options
|
||||||
|
* - where are the default values
|
||||||
|
* - which fields are colours
|
||||||
|
* - which fields are persisted
|
||||||
*/
|
*/
|
||||||
function collectFieldMetadata(paramFields: ViewOption[]): FieldMetadata {
|
function collectFieldMetadata(paramFields: ViewOption[]): FieldMetadata {
|
||||||
const metadata: FieldMetadata = {
|
const metadata: FieldMetadata = {
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
colorFields: new Set(),
|
colorFields: new Set(),
|
||||||
|
booleanFields: new Set(),
|
||||||
isPersistedField: new Set(),
|
isPersistedField: new Set(),
|
||||||
persistedValues: {},
|
persistedValues: {},
|
||||||
};
|
};
|
||||||
@@ -80,6 +101,8 @@ function collectFieldMetadata(paramFields: ViewOption[]): FieldMetadata {
|
|||||||
|
|
||||||
if (option.type === 'colour') {
|
if (option.type === 'colour') {
|
||||||
metadata.colorFields.add(option.id);
|
metadata.colorFields.add(option.id);
|
||||||
|
} else if (option.type === 'boolean') {
|
||||||
|
metadata.booleanFields.add(option.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -132,7 +155,16 @@ export function getURLSearchParamsFromObj(paramsObj: ViewParamsObj, paramFields:
|
|||||||
|
|
||||||
// Process and add new values
|
// Process and add new values
|
||||||
value.split(',').forEach((v) => {
|
value.split(',').forEach((v) => {
|
||||||
const processedValue = metadata.colorFields.has(id) ? sanitiseColour(v) : v;
|
// some field types need extra processing
|
||||||
|
const processedValue = (() => {
|
||||||
|
if (metadata.colorFields.has(id)) {
|
||||||
|
return sanitiseColour(v);
|
||||||
|
}
|
||||||
|
if (metadata.booleanFields.has(id)) {
|
||||||
|
return v === 'on' ? 'true' : 'false';
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
})();
|
||||||
if (metadata.isPersistedField.has(id) || metadata.defaultValues[id] !== processedValue) {
|
if (metadata.isPersistedField.has(id) || metadata.defaultValues[id] !== processedValue) {
|
||||||
addUniqueParam(id, processedValue);
|
addUniqueParam(id, processedValue);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { MouseEvent } from 'react';
|
import { MouseEvent } from 'react';
|
||||||
|
|
||||||
import { Option, useContextMenuStore } from '../components/context-menu/ContextMenu';
|
import { useContextMenuStore } from '../../features/rundown/rundown-context-menu/RundownContextMenu';
|
||||||
|
import { DropdownMenuOption } from '../components/dropdown-menu/DropdownMenu';
|
||||||
|
|
||||||
export const useContextMenu = <T extends HTMLElement>(options: Option[]) => {
|
export const useContextMenu = <T extends HTMLElement>(options: DropdownMenuOption[]) => {
|
||||||
const { setContextMenu } = useContextMenuStore();
|
const setContextMenu = useContextMenuStore((state) => state.setContextMenu);
|
||||||
|
|
||||||
const localCreateContextMenu = (contextMenuEvent: MouseEvent<T, globalThis.MouseEvent>) => {
|
const localCreateContextMenu = (contextMenuEvent: MouseEvent<T, globalThis.MouseEvent>) => {
|
||||||
// prevent browser default context menu from showing up
|
// prevent browser default context menu from showing up
|
||||||
|
|||||||
@@ -150,9 +150,9 @@ $inner-padding: 1rem;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
|
border: none;
|
||||||
border-top: 1px solid $white-10;
|
border-top: 1px solid $white-10;
|
||||||
margin: 1rem -2rem;
|
margin: 1rem -2rem;
|
||||||
z-index: $zindex-floating;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay {
|
.overlay {
|
||||||
|
|||||||
-5
@@ -13,11 +13,6 @@
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.matchRadio {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ruleSection {
|
.ruleSection {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { Controller, 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 { Radio, RadioGroup, Select } from '@chakra-ui/react';
|
|
||||||
import {
|
import {
|
||||||
Automation,
|
Automation,
|
||||||
AutomationDTO,
|
AutomationDTO,
|
||||||
@@ -20,6 +19,8 @@ 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 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 RadioGroup from '../../../../common/components/radio-group/RadioGroup';
|
||||||
|
import Select from '../../../../common/components/select/Select';
|
||||||
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 useCustomFields from '../../../../common/hooks-query/useCustomFields';
|
import useCustomFields from '../../../../common/hooks-query/useCustomFields';
|
||||||
@@ -214,15 +215,14 @@ export default function AutomationForm(props: AutomationFormProps) {
|
|||||||
<div className={style.ruleSection}>
|
<div className={style.ruleSection}>
|
||||||
<label>
|
<label>
|
||||||
Trigger outputs if
|
Trigger outputs if
|
||||||
<Controller
|
<RadioGroup
|
||||||
name='filterRule'
|
orientation='horizontal'
|
||||||
control={control}
|
value={watch('filterRule')}
|
||||||
render={({ field }) => (
|
onValueChange={(value) => setValue('filterRule', value, { shouldDirty: true })}
|
||||||
<RadioGroup {...field} size='sm' className={style.matchRadio} variant='ontime'>
|
items={[
|
||||||
<Radio value='all'>All filters pass</Radio>
|
{ value: 'all', label: 'All filters pass' },
|
||||||
<Radio value='any'>Any filter passes</Radio>
|
{ value: 'any', label: 'Any filter passes' },
|
||||||
</RadioGroup>
|
]}
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
{fieldFilters.map((field, index) => {
|
{fieldFilters.map((field, index) => {
|
||||||
@@ -232,43 +232,29 @@ export default function AutomationForm(props: AutomationFormProps) {
|
|||||||
<label>
|
<label>
|
||||||
Runtime data source
|
Runtime data source
|
||||||
<Select
|
<Select
|
||||||
{...register(`filters.${index}.field`, { required: { value: true, message: 'Required field' } })}
|
value={watch(`filters.${index}.field`)}
|
||||||
size='sm'
|
onValueChange={(value) => setValue(`filters.${index}.field`, value, { shouldDirty: true })}
|
||||||
variant='ontime'
|
options={fieldList.map(({ value, label }) => ({ value, label }))}
|
||||||
>
|
aria-label='Event field'
|
||||||
<option selected hidden disabled value=''>
|
/>
|
||||||
Event field
|
|
||||||
</option>
|
|
||||||
{fieldList.map(({ value, label }, localIndex) => {
|
|
||||||
const key = `filters.${index}.field.${localIndex}`;
|
|
||||||
return (
|
|
||||||
<option key={key} value={value}>
|
|
||||||
{label}
|
|
||||||
</option>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Select>
|
|
||||||
<Panel.Error>{errors.filters?.[index]?.field?.message}</Panel.Error>
|
<Panel.Error>{errors.filters?.[index]?.field?.message}</Panel.Error>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Matching condition
|
Matching condition
|
||||||
<Select
|
<Select
|
||||||
{...register(`filters.${index}.operator`, { required: { value: true, message: 'Required field' } })}
|
value={watch(`filters.${index}.operator`)}
|
||||||
size='sm'
|
onValueChange={(value) =>
|
||||||
variant='ontime'
|
setValue(`filters.${index}.operator`, value as 'equals' | 'not_equals' | 'contains', {
|
||||||
>
|
shouldDirty: true,
|
||||||
<option selected hidden disabled value=''>
|
})
|
||||||
Operator
|
}
|
||||||
</option>
|
options={[
|
||||||
<option value='equals'>equals</option>
|
{ value: 'equals', label: 'equals' },
|
||||||
<option value='not_equals'>not equals</option>
|
{ value: 'not_equals', label: 'not equals' },
|
||||||
<option value='contains'>contains</option>
|
{ value: 'contains', label: 'contains' },
|
||||||
{/*
|
]}
|
||||||
We dont currently offer a data source where these operators would make sense
|
aria-label='Operator'
|
||||||
<option value='greater_than'>greater than</option>
|
/>
|
||||||
<option value='less_than'>less than</option>
|
|
||||||
*/}
|
|
||||||
</Select>
|
|
||||||
<Panel.Error>{errors.filters?.[index]?.operator?.message}</Panel.Error>
|
<Panel.Error>{errors.filters?.[index]?.operator?.message}</Panel.Error>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
|
|||||||
+12
-15
@@ -1,5 +1,4 @@
|
|||||||
import { Controller, useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { 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';
|
||||||
@@ -7,6 +6,7 @@ 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 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 Switch from '../../../../common/components/switch/Switch';
|
||||||
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';
|
||||||
@@ -24,11 +24,12 @@ export default function AutomationSettingsForm(props: AutomationSettingsProps) {
|
|||||||
const { enabledAutomations, enabledOscIn, oscPortIn } = props;
|
const { enabledAutomations, enabledOscIn, oscPortIn } = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
register,
|
register,
|
||||||
setError,
|
setError,
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
formState: { errors, isSubmitting, isDirty, isValid },
|
formState: { errors, isSubmitting, isDirty, isValid },
|
||||||
} = useForm<AutomationSettingsProps>({
|
} = useForm<AutomationSettingsProps>({
|
||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
@@ -103,12 +104,10 @@ export default function AutomationSettingsForm(props: AutomationSettingsProps) {
|
|||||||
description='Allow Ontime to send messages on lifecycle triggers'
|
description='Allow Ontime to send messages on lifecycle triggers'
|
||||||
error={errors.enabledAutomations?.message}
|
error={errors.enabledAutomations?.message}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Switch
|
||||||
control={control}
|
size='large'
|
||||||
name='enabledAutomations'
|
checked={watch('enabledAutomations')}
|
||||||
render={({ field: { onChange, value, ref } }) => (
|
onCheckedChange={(value: boolean) => setValue('enabledAutomations', value, { shouldDirty: true })}
|
||||||
<Switch variant='ontime' size='lg' isChecked={value} onChange={onChange} ref={ref} />
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
</Panel.ListGroup>
|
</Panel.ListGroup>
|
||||||
@@ -123,12 +122,10 @@ export default function AutomationSettingsForm(props: AutomationSettingsProps) {
|
|||||||
description='Allow control of Ontime through OSC'
|
description='Allow control of Ontime through OSC'
|
||||||
error={errors.enabledOscIn?.message}
|
error={errors.enabledOscIn?.message}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Switch
|
||||||
control={control}
|
size='large'
|
||||||
name='enabledOscIn'
|
checked={watch('enabledOscIn')}
|
||||||
render={({ field: { onChange, value, ref } }) => (
|
onCheckedChange={(value: boolean) => setValue('enabledOscIn', value, { shouldDirty: true })}
|
||||||
<Switch variant='ontime' size='lg' isChecked={value} onChange={onChange} ref={ref} />
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
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 { 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 Button from '../../../../common/components/buttons/Button';
|
||||||
import Input from '../../../../common/components/input/input/Input';
|
import Input from '../../../../common/components/input/input/Input';
|
||||||
|
import Select from '../../../../common/components/select/Select';
|
||||||
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';
|
||||||
|
|
||||||
@@ -29,6 +29,8 @@ export default function TriggerForm(props: TriggerFormProps) {
|
|||||||
register,
|
register,
|
||||||
setFocus,
|
setFocus,
|
||||||
setError,
|
setError,
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
formState: { errors, isSubmitting, isValid, isDirty },
|
formState: { errors, isSubmitting, isValid, isDirty },
|
||||||
} = useForm<TriggerDTO>({
|
} = useForm<TriggerDTO>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -44,8 +46,7 @@ export default function TriggerForm(props: TriggerFormProps) {
|
|||||||
// give initial focus to the title field
|
// give initial focus to the title field
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFocus('title');
|
setFocus('title');
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- focus on mount
|
}, [setFocus]);
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onSubmit = async (values: TriggerDTO) => {
|
const onSubmit = async (values: TriggerDTO) => {
|
||||||
// if we were passed an ID we are editing a Trigger
|
// if we were passed an ID we are editing a Trigger
|
||||||
@@ -97,33 +98,21 @@ export default function TriggerForm(props: TriggerFormProps) {
|
|||||||
<label>
|
<label>
|
||||||
Lifecycle trigger
|
Lifecycle trigger
|
||||||
<Select
|
<Select
|
||||||
size='sm'
|
value={watch('trigger')}
|
||||||
variant='ontime'
|
onValueChange={(value) => setValue('trigger', value as TimerLifeCycle, { shouldDirty: true })}
|
||||||
defaultValue={initialTrigger}
|
options={cycles.map((cycle) => ({ value: cycle.value, label: cycle.label }))}
|
||||||
{...register('trigger', { required: { value: true, message: 'Required field' } })}
|
aria-label='Lifecycle trigger'
|
||||||
>
|
/>
|
||||||
{cycles.map((cycle) => (
|
|
||||||
<option key={cycle.id} value={cycle.value}>
|
|
||||||
{cycle.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
<Panel.Error>{errors.trigger?.message}</Panel.Error>
|
<Panel.Error>{errors.trigger?.message}</Panel.Error>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Automation title
|
Automation title
|
||||||
<Select
|
<Select
|
||||||
size='sm'
|
value={watch('automationId')}
|
||||||
variant='ontime'
|
onValueChange={(value) => setValue('automationId', value, { shouldDirty: true })}
|
||||||
defaultValue={initialAutomationId}
|
options={automationSelect}
|
||||||
{...register('automationId', { required: { value: true, message: 'Required field' } })}
|
aria-label='Automation title'
|
||||||
>
|
/>
|
||||||
{automationSelect.map((automation) => (
|
|
||||||
<option key={automation.value} value={automation.value}>
|
|
||||||
{automation.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
<Panel.Error>{errors.automationId?.message}</Panel.Error>
|
<Panel.Error>{errors.automationId?.message}</Panel.Error>
|
||||||
</label>
|
</label>
|
||||||
<Panel.InlineElements align='end'>
|
<Panel.InlineElements align='end'>
|
||||||
|
|||||||
@@ -107,11 +107,17 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, isLockedToV
|
|||||||
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
|
||||||
|
size='large'
|
||||||
|
name='lock'
|
||||||
|
checked={watch('lock')}
|
||||||
|
onCheckedChange={(checked) => setValue('lock', checked)}
|
||||||
|
/>
|
||||||
</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
|
||||||
|
size='large'
|
||||||
name='authenticate'
|
name='authenticate'
|
||||||
checked={watch('authenticate')}
|
checked={watch('authenticate')}
|
||||||
onCheckedChange={(checked) => setValue('authenticate', checked)}
|
onCheckedChange={(checked) => setValue('authenticate', checked)}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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 { URLPreset } from 'ontime-types';
|
import { URLPreset } from 'ontime-types';
|
||||||
|
|
||||||
import { postUrlPresets } from '../../../../common/api/urlPresets';
|
import { postUrlPresets } from '../../../../common/api/urlPresets';
|
||||||
@@ -11,6 +10,7 @@ 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 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 Switch from '../../../../common/components/switch/Switch';
|
||||||
import Tooltip from '../../../../common/components/tooltip/Tooltip';
|
import Tooltip from '../../../../common/components/tooltip/Tooltip';
|
||||||
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';
|
||||||
@@ -34,6 +34,8 @@ export default function UrlPresetsForm() {
|
|||||||
register,
|
register,
|
||||||
reset,
|
reset,
|
||||||
setError,
|
setError,
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
formState: { isSubmitting, isDirty, isValid, errors },
|
formState: { isSubmitting, isDirty, isValid, errors },
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
@@ -166,8 +168,11 @@ export default function UrlPresetsForm() {
|
|||||||
<tr key={preset.id}>
|
<tr key={preset.id}>
|
||||||
<td className={style.fit}>
|
<td className={style.fit}>
|
||||||
<Switch
|
<Switch
|
||||||
{...register(`data.${index}.enabled`)}
|
size='large'
|
||||||
variant='ontime'
|
checked={watch(`data.${index}.enabled`)}
|
||||||
|
onCheckedChange={(value: boolean) =>
|
||||||
|
setValue(`data.${index}.enabled`, value, { shouldDirty: true })
|
||||||
|
}
|
||||||
data-testid={`field__enable_${index}`}
|
data-testid={`field__enable_${index}`}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
+24
-34
@@ -1,8 +1,9 @@
|
|||||||
import { Select, Switch } from '@chakra-ui/react';
|
|
||||||
import { EndAction, TimerType, TimeStrategy } from 'ontime-types';
|
import { EndAction, TimerType, TimeStrategy } from 'ontime-types';
|
||||||
import { parseUserTime } from 'ontime-utils';
|
import { parseUserTime } from 'ontime-utils';
|
||||||
|
|
||||||
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
|
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
|
||||||
|
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';
|
||||||
|
|
||||||
@@ -41,12 +42,7 @@ export default function RundownDefaultSettings() {
|
|||||||
title='Link previous'
|
title='Link previous'
|
||||||
description='Whether the start time of new events should be linked to the previous event end time'
|
description='Whether the start time of new events should be linked to the previous event end time'
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch size='large' checked={linkPrevious} onCheckedChange={setLinkPrevious} />
|
||||||
variant='ontime'
|
|
||||||
size='lg'
|
|
||||||
defaultChecked={linkPrevious}
|
|
||||||
onChange={(event) => setLinkPrevious(event.target.checked)}
|
|
||||||
/>
|
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field
|
<Panel.Field
|
||||||
@@ -54,15 +50,13 @@ export default function RundownDefaultSettings() {
|
|||||||
description='Which time should be maintained when event schedule is recalculated'
|
description='Which time should be maintained when event schedule is recalculated'
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
variant='ontime'
|
|
||||||
size='sm'
|
|
||||||
width='auto'
|
|
||||||
value={defaultTimeStrategy}
|
value={defaultTimeStrategy}
|
||||||
onChange={(event) => setTimeStrategy(event.target.value as TimeStrategy)}
|
onValueChange={(value) => setTimeStrategy(value as TimeStrategy)}
|
||||||
>
|
options={[
|
||||||
<option value={TimeStrategy.LockDuration}>Duration</option>
|
{ value: TimeStrategy.LockDuration, label: 'Duration' },
|
||||||
<option value={TimeStrategy.LockEnd}>End Time</option>
|
{ value: TimeStrategy.LockEnd, label: 'End Time' },
|
||||||
</Select>
|
]}
|
||||||
|
/>
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
</Panel.ListGroup>
|
</Panel.ListGroup>
|
||||||
<Panel.ListGroup>
|
<Panel.ListGroup>
|
||||||
@@ -78,31 +72,27 @@ export default function RundownDefaultSettings() {
|
|||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field title='Timer type' description='Default type of timer for new events' />
|
<Panel.Field title='Timer type' description='Default type of timer for new events' />
|
||||||
<Select
|
<Select
|
||||||
variant='ontime'
|
|
||||||
size='sm'
|
|
||||||
width='auto'
|
|
||||||
value={defaultTimerType}
|
value={defaultTimerType}
|
||||||
onChange={(event) => setDefaultTimerType(event.target.value as TimerType)}
|
onValueChange={(value) => setDefaultTimerType(value as TimerType)}
|
||||||
>
|
options={[
|
||||||
<option value={TimerType.CountDown}>Count down</option>
|
{ value: TimerType.CountDown, label: 'Count down' },
|
||||||
<option value={TimerType.CountUp}>Count up</option>
|
{ value: TimerType.CountUp, label: 'Count up' },
|
||||||
<option value={TimerType.Clock}>Clock</option>
|
{ value: TimerType.Clock, label: 'Clock' },
|
||||||
<option value={TimerType.None}>None</option>
|
{ value: TimerType.None, label: 'None' },
|
||||||
</Select>
|
]}
|
||||||
|
/>
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field title='End Action' description='Default end action for new events' />
|
<Panel.Field title='End Action' description='Default end action for new events' />
|
||||||
<Select
|
<Select
|
||||||
variant='ontime'
|
|
||||||
size='sm'
|
|
||||||
width='auto'
|
|
||||||
value={defaultEndAction}
|
value={defaultEndAction}
|
||||||
onChange={(event) => setDefaultEndAction(event.target.value as EndAction)}
|
onValueChange={(value) => setDefaultEndAction(value as EndAction)}
|
||||||
>
|
options={[
|
||||||
<option value={EndAction.None}>None</option>
|
{ value: EndAction.None, label: 'None' },
|
||||||
<option value={EndAction.LoadNext}>Load next</option>
|
{ value: EndAction.LoadNext, label: 'Load next' },
|
||||||
<option value={EndAction.PlayNext}>Play next</option>
|
{ value: EndAction.PlayNext, label: 'Play next' },
|
||||||
</Select>
|
]}
|
||||||
|
/>
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
</Panel.ListGroup>
|
</Panel.ListGroup>
|
||||||
<Panel.ListGroup>
|
<Panel.ListGroup>
|
||||||
|
|||||||
+1
-1
@@ -56,7 +56,7 @@ export default function CustomFieldEntry(props: CustomFieldEntryProps) {
|
|||||||
</td>
|
</td>
|
||||||
<td className={style.halfWidth}>{label}</td>
|
<td className={style.halfWidth}>{label}</td>
|
||||||
<td className={style.fullWidth}>
|
<td className={style.fullWidth}>
|
||||||
<CopyTag label='Copy key to use in integrations' copyValue={fieldKey}>
|
<CopyTag size='small' copyValue={fieldKey}>
|
||||||
{fieldKey}
|
{fieldKey}
|
||||||
</CopyTag>
|
</CopyTag>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
+13
-15
@@ -1,6 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { 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';
|
||||||
|
|
||||||
@@ -9,6 +8,7 @@ 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 Input from '../../../../../common/components/input/input/Input';
|
||||||
|
import RadioGroup from '../../../../../common/components/radio-group/RadioGroup';
|
||||||
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';
|
||||||
@@ -33,13 +33,13 @@ export default function CustomFieldForm(props: CustomFieldsFormProps) {
|
|||||||
const [_, setColour] = useState(initialColour || '');
|
const [_, setColour] = useState(initialColour || '');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
register,
|
register,
|
||||||
setFocus,
|
setFocus,
|
||||||
setError,
|
setError,
|
||||||
setValue,
|
setValue,
|
||||||
getValues,
|
getValues,
|
||||||
|
watch,
|
||||||
formState: { errors, isSubmitting, isValid, isDirty },
|
formState: { errors, isSubmitting, isValid, isDirty },
|
||||||
} = useForm<CustomFieldFormData>({
|
} = useForm<CustomFieldFormData>({
|
||||||
defaultValues: { type: 'string', label: initialLabel || '', colour: initialColour || '' },
|
defaultValues: { type: 'string', label: initialLabel || '', colour: initialColour || '' },
|
||||||
@@ -90,17 +90,15 @@ export default function CustomFieldForm(props: CustomFieldsFormProps) {
|
|||||||
</Info>
|
</Info>
|
||||||
<div>
|
<div>
|
||||||
<Panel.Description>Type</Panel.Description>
|
<Panel.Description>Type</Panel.Description>
|
||||||
<Controller
|
<RadioGroup
|
||||||
name='type'
|
orientation='horizontal'
|
||||||
control={control}
|
disabled={isEditMode}
|
||||||
render={({ field }) => (
|
onValueChange={(value) => setValue('type', value, { shouldDirty: true })}
|
||||||
<RadioGroup {...field} size='sm' isDisabled={isEditMode} variant='ontime'>
|
value={watch('type')}
|
||||||
<Panel.InlineElements relation='component'>
|
items={[
|
||||||
<Radio value='string'>Text</Radio>
|
{ value: 'string', label: 'Text' },
|
||||||
<Radio value='image'>Image</Radio>
|
{ value: 'image', label: 'Image' },
|
||||||
</Panel.InlineElements>
|
]}
|
||||||
</RadioGroup>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.twoCols}>
|
<div className={style.twoCols}>
|
||||||
@@ -138,7 +136,7 @@ export default function CustomFieldForm(props: CustomFieldsFormProps) {
|
|||||||
<Button variant='ghosted' onClick={onCancel}>
|
<Button variant='ghosted' onClick={onCancel}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type='submit' variant='primary' disabled={!canSubmit} loading={isSubmitting}>
|
<Button type='submit' variant='primary' disabled={!canSubmit} loading={isSubmitting}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
|
|||||||
+1
-1
@@ -174,7 +174,7 @@ export default function GSheetSetup(props: GSheetSetupProps) {
|
|||||||
<Panel.ListGroup>
|
<Panel.ListGroup>
|
||||||
<Panel.InlineElements>
|
<Panel.InlineElements>
|
||||||
{isAuthenticating && <span>Authenticating...</span>}
|
{isAuthenticating && <span>Authenticating...</span>}
|
||||||
<CopyTag copyValue={authKey ?? ''} label='Google Auth Key' disabled={!canAuthenticate} size='sm'>
|
<CopyTag copyValue={authKey ?? ''} disabled={!canAuthenticate}>
|
||||||
{authKey ? authKey : 'Upload files to generate Auth Key'}
|
{authKey ? authKey : 'Upload files to generate Auth Key'}
|
||||||
</CopyTag>
|
</CopyTag>
|
||||||
<Button onClick={handleAuthenticate} disabled={!canAuthenticate}>
|
<Button onClick={handleAuthenticate} disabled={!canAuthenticate}>
|
||||||
|
|||||||
+8
-13
@@ -1,12 +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 { IoAdd, IoTrash } from 'react-icons/io5';
|
||||||
import { Select } from '@chakra-ui/react';
|
|
||||||
import { ImportMap, isAlphanumericWithSpace } from 'ontime-utils';
|
import { ImportMap, isAlphanumericWithSpace } from 'ontime-utils';
|
||||||
|
|
||||||
import Button from '../../../../../../common/components/buttons/Button';
|
import Button from '../../../../../../common/components/buttons/Button';
|
||||||
import IconButton from '../../../../../../common/components/buttons/IconButton';
|
import IconButton from '../../../../../../common/components/buttons/IconButton';
|
||||||
import Input from '../../../../../../common/components/input/input/Input';
|
import Input from '../../../../../../common/components/input/input/Input';
|
||||||
|
import Select from '../../../../../../common/components/select/Select';
|
||||||
import Tooltip from '../../../../../../common/components/tooltip/Tooltip';
|
import Tooltip from '../../../../../../common/components/tooltip/Tooltip';
|
||||||
import * as Panel from '../../../../panel-utils/PanelUtils';
|
import * as Panel from '../../../../panel-utils/PanelUtils';
|
||||||
import useGoogleSheet from '../useGoogleSheet';
|
import useGoogleSheet from '../useGoogleSheet';
|
||||||
@@ -33,6 +33,7 @@ export default function ImportMapForm(props: ImportMapFormProps) {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
register,
|
register,
|
||||||
setValue,
|
setValue,
|
||||||
|
watch,
|
||||||
formState: { errors, isValid },
|
formState: { errors, isValid },
|
||||||
} = useForm<NamedImportMap>({
|
} = useForm<NamedImportMap>({
|
||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
@@ -149,19 +150,13 @@ export default function ImportMapForm(props: ImportMapFormProps) {
|
|||||||
<td>{label}</td>
|
<td>{label}</td>
|
||||||
<td>
|
<td>
|
||||||
<Select
|
<Select
|
||||||
variant='ontime'
|
|
||||||
id={importName as string}
|
id={importName as string}
|
||||||
size='sm'
|
value={watch(label as keyof NamedImportMap) as string}
|
||||||
{...register(label as keyof NamedImportMap)}
|
onValueChange={(value: string) =>
|
||||||
>
|
setValue(label as keyof NamedImportMap, value, { shouldDirty: true })
|
||||||
{worksheetNames?.map((name) => {
|
}
|
||||||
return (
|
options={worksheetNames?.map((name) => ({ value: name, label: name })) || []}
|
||||||
<option key={name} value={name}>
|
/>
|
||||||
{name}
|
|
||||||
</option>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Select>
|
|
||||||
</td>
|
</td>
|
||||||
<td className={style.singleActionCell} />
|
<td className={style.singleActionCell} />
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -21,12 +21,7 @@ export default function InfoNif() {
|
|||||||
const address = linkToOtherHost(nif.address);
|
const address = linkToOtherHost(nif.address);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CopyTag
|
<CopyTag key={nif.name} copyValue={address} onClick={() => handleClick(address)}>
|
||||||
key={nif.name}
|
|
||||||
copyValue={address}
|
|
||||||
onClick={() => handleClick(address)}
|
|
||||||
label='Copy IP or navigate to address'
|
|
||||||
>
|
|
||||||
{`${nif.name} - ${nif.address}`} <IoArrowUp className={style.goIcon} />
|
{`${nif.name} - ${nif.address}`} <IoArrowUp className={style.goIcon} />
|
||||||
</CopyTag>
|
</CopyTag>
|
||||||
);
|
);
|
||||||
|
|||||||
+5
@@ -10,3 +10,8 @@
|
|||||||
width: 50%;
|
width: 50%;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.copiable {
|
||||||
|
cursor: text ;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
+3
-3
@@ -61,8 +61,8 @@ export default function ClientList() {
|
|||||||
<Panel.Table>
|
<Panel.Table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td className={style.halfWidth}>Client Name</td>
|
<td style={{ width: '20%' }}>Client Name</td>
|
||||||
<td className={style.fullWidth}>Path</td>
|
<td>Path</td>
|
||||||
<td />
|
<td />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -76,7 +76,7 @@ export default function ClientList() {
|
|||||||
{isCurrent && <Tag>SELF</Tag>}
|
{isCurrent && <Tag>SELF</Tag>}
|
||||||
{name}
|
{name}
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
<td>{path}</td>
|
<td className={style.copiable}>{path}</td>
|
||||||
<Panel.InlineElements relation='inner'>
|
<Panel.InlineElements relation='inner'>
|
||||||
<Button
|
<Button
|
||||||
size='small'
|
size='small'
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { IoEllipsisHorizontal } from 'react-icons/io5';
|
import {
|
||||||
import { IconButton, Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/react';
|
IoCopyOutline,
|
||||||
|
IoDocumentOutline,
|
||||||
|
IoDownloadOutline,
|
||||||
|
IoEllipsisHorizontal,
|
||||||
|
IoPencilOutline,
|
||||||
|
IoTrash,
|
||||||
|
} from 'react-icons/io5';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
deleteProject,
|
deleteProject,
|
||||||
@@ -11,6 +17,8 @@ import {
|
|||||||
renameProject,
|
renameProject,
|
||||||
} from '../../../../common/api/db';
|
} from '../../../../common/api/db';
|
||||||
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/utils';
|
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/utils';
|
||||||
|
import IconButton from '../../../../common/components/buttons/IconButton';
|
||||||
|
import { DropdownMenu } from '../../../../common/components/dropdown-menu/DropdownMenu';
|
||||||
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';
|
||||||
|
|
||||||
@@ -184,31 +192,33 @@ function ActionMenu(props: ActionMenuProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu variant='ontime-on-dark' size='sm'>
|
<DropdownMenu
|
||||||
<MenuButton
|
render={<IconButton variant='ghosted-white' />}
|
||||||
as={IconButton}
|
disabled={isDisabled}
|
||||||
aria-label='Options'
|
items={[
|
||||||
icon={<IoEllipsisHorizontal />}
|
{
|
||||||
color='#e2e2e2' // $gray-200
|
type: 'item',
|
||||||
variant='ontime-ghosted'
|
icon: IoDownloadOutline,
|
||||||
size='sm'
|
label: 'Load',
|
||||||
isDisabled={isDisabled}
|
onClick: () => onLoad(filename),
|
||||||
/>
|
disabled: current,
|
||||||
<MenuList>
|
},
|
||||||
<MenuItem onClick={() => onLoad(filename)} isDisabled={current}>
|
{
|
||||||
Load
|
type: 'item',
|
||||||
</MenuItem>
|
icon: IoDownloadOutline,
|
||||||
<MenuItem onClick={() => onMerge(filename)} isDisabled={current}>
|
label: 'Partial Load',
|
||||||
Partial Load
|
onClick: () => onMerge(filename),
|
||||||
</MenuItem>
|
disabled: current,
|
||||||
<MenuItem onClick={handleRename}>Rename</MenuItem>
|
},
|
||||||
<MenuItem onClick={handleDuplicate}>Duplicate</MenuItem>
|
{ type: 'item', icon: IoPencilOutline, label: 'Rename', onClick: handleRename },
|
||||||
<MenuItem onClick={handleDownload}>Download</MenuItem>
|
{ type: 'item', icon: IoCopyOutline, label: 'Duplicate', onClick: handleDuplicate },
|
||||||
<MenuItem onClick={handleExportCSV}>Export CSV Rundown</MenuItem>
|
{ type: 'item', icon: IoDocumentOutline, label: 'Download', onClick: handleDownload },
|
||||||
<MenuItem isDisabled={current} onClick={() => onDelete(filename)}>
|
{ type: 'item', icon: IoDocumentOutline, label: 'Export CSV Rundown', onClick: handleExportCSV },
|
||||||
Delete
|
{ type: 'divider' },
|
||||||
</MenuItem>
|
{ type: 'item', icon: IoTrash, label: 'Delete', onClick: () => onDelete(filename), disabled: current },
|
||||||
</MenuList>
|
]}
|
||||||
</Menu>
|
>
|
||||||
|
<IoEllipsisHorizontal />
|
||||||
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
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 { 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 Button from '../../../../common/components/buttons/Button';
|
||||||
|
import Switch from '../../../../common/components/switch/Switch';
|
||||||
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';
|
||||||
|
|
||||||
@@ -34,7 +34,8 @@ export default function ProjectMergeForm(props: ProjectMergeFromProps) {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
register,
|
watch,
|
||||||
|
setValue,
|
||||||
formState: { isSubmitting, isValid, isDirty },
|
formState: { isSubmitting, isValid, isDirty },
|
||||||
} = useForm<ProjectMergeFormValues>({
|
} = useForm<ProjectMergeFormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -92,23 +93,43 @@ export default function ProjectMergeForm(props: ProjectMergeFromProps) {
|
|||||||
<br /> This process is irreversible.
|
<br /> This process is irreversible.
|
||||||
</Panel.Description>
|
</Panel.Description>
|
||||||
<label>
|
<label>
|
||||||
<Switch variant='ontime' {...register('project')} />
|
<Switch
|
||||||
|
size='large'
|
||||||
|
checked={watch('project')}
|
||||||
|
onCheckedChange={(value: boolean) => setValue('project', value, { shouldDirty: true })}
|
||||||
|
/>
|
||||||
Project data
|
Project data
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<Switch variant='ontime' {...register('rundown')} />
|
<Switch
|
||||||
|
size='large'
|
||||||
|
checked={watch('rundown')}
|
||||||
|
onCheckedChange={(value: boolean) => setValue('rundown', value, { shouldDirty: true })}
|
||||||
|
/>
|
||||||
Rundown + Custom Fields
|
Rundown + Custom Fields
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<Switch variant='ontime' {...register('viewSettings')} />
|
<Switch
|
||||||
|
size='large'
|
||||||
|
checked={watch('viewSettings')}
|
||||||
|
onCheckedChange={(value: boolean) => setValue('viewSettings', value, { shouldDirty: true })}
|
||||||
|
/>
|
||||||
View Settings
|
View Settings
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<Switch variant='ontime' {...register('urlPresets')} />
|
<Switch
|
||||||
|
size='large'
|
||||||
|
checked={watch('urlPresets')}
|
||||||
|
onCheckedChange={(value: boolean) => setValue('urlPresets', value, { shouldDirty: true })}
|
||||||
|
/>
|
||||||
URL Presets
|
URL Presets
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<Switch variant='ontime' {...register('automation')} />
|
<Switch
|
||||||
|
size='large'
|
||||||
|
checked={watch('automation')}
|
||||||
|
onCheckedChange={(value: boolean) => setValue('automation', value, { shouldDirty: true })}
|
||||||
|
/>
|
||||||
Automation Settings
|
Automation Settings
|
||||||
</label>
|
</label>
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export default function GeneralSettings() {
|
|||||||
description='Protect the editor view with a pin code'
|
description='Protect the editor view with a pin code'
|
||||||
error={errors.editorKey?.message}
|
error={errors.editorKey?.message}
|
||||||
/>
|
/>
|
||||||
<GeneralPinInput register={register} formName='editorKey' isDisabled={disableInputs} />
|
<GeneralPinInput register={register} formName='editorKey' disabled={disableInputs} />
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field
|
<Panel.Field
|
||||||
@@ -127,7 +127,7 @@ export default function GeneralSettings() {
|
|||||||
description='Protect the operator and cuesheet views with a pin code'
|
description='Protect the operator and cuesheet views with a pin code'
|
||||||
error={errors.operatorKey?.message}
|
error={errors.operatorKey?.message}
|
||||||
/>
|
/>
|
||||||
<GeneralPinInput register={register} formName='operatorKey' isDisabled={disableInputs} />
|
<GeneralPinInput register={register} formName='operatorKey' disabled={disableInputs} />
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field
|
<Panel.Field
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { Switch } from '@chakra-ui/react';
|
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import { ViewSettings as ViewSettingsType } from 'ontime-types';
|
import { ViewSettings as ViewSettingsType } from 'ontime-types';
|
||||||
|
|
||||||
@@ -10,6 +9,7 @@ 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 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 Switch from '../../../../common/components/switch/Switch';
|
||||||
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 * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
@@ -28,6 +28,8 @@ export default function ViewSettings() {
|
|||||||
setError,
|
setError,
|
||||||
register,
|
register,
|
||||||
reset,
|
reset,
|
||||||
|
setValue,
|
||||||
|
watch,
|
||||||
formState: { isSubmitting, isDirty, errors },
|
formState: { isSubmitting, isDirty, errors },
|
||||||
} = useForm<ViewSettingsType>({
|
} = useForm<ViewSettingsType>({
|
||||||
defaultValues: data,
|
defaultValues: data,
|
||||||
@@ -96,12 +98,10 @@ export default function ViewSettings() {
|
|||||||
title='Override CSS styles'
|
title='Override CSS styles'
|
||||||
description='Enables overriding view styles with custom stylesheet'
|
description='Enables overriding view styles with custom stylesheet'
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Switch
|
||||||
control={control}
|
size='large'
|
||||||
name='overrideStyles'
|
checked={watch('overrideStyles')}
|
||||||
render={({ field: { onChange, value, ref } }) => (
|
onCheckedChange={(value: boolean) => setValue('overrideStyles', value, { shouldDirty: true })}
|
||||||
<Switch variant='ontime' size='lg' isChecked={value} onChange={onChange} ref={ref} />
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
<Button onClick={codeEditorHandler.open} disabled={isSubmitting}>
|
<Button onClick={codeEditorHandler.open} disabled={isSubmitting}>
|
||||||
Edit CSS override
|
Edit CSS override
|
||||||
@@ -128,12 +128,10 @@ export default function ViewSettings() {
|
|||||||
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.'
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Switch
|
||||||
control={control}
|
size='large'
|
||||||
name='freezeEnd'
|
checked={watch('freezeEnd')}
|
||||||
render={({ field: { onChange, value, ref } }) => (
|
onCheckedChange={(value: boolean) => setValue('freezeEnd', value, { shouldDirty: true })}
|
||||||
<Switch variant='ontime' size='lg' isChecked={value} onChange={onChange} ref={ref} />
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
|
|||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
-18
@@ -1,24 +1,26 @@
|
|||||||
import { PropsWithChildren, useState } from 'react';
|
import { PropsWithChildren, useState } from 'react';
|
||||||
import { UseFormRegister } from 'react-hook-form';
|
import { UseFormRegister } from 'react-hook-form';
|
||||||
import { IoEyeOutline } from 'react-icons/io5';
|
import { IoEyeOutline } from 'react-icons/io5';
|
||||||
import { IconButton, Input, InputGroup, InputRightElement } from '@chakra-ui/react';
|
|
||||||
import { Settings } from 'ontime-types';
|
import { Settings } from 'ontime-types';
|
||||||
|
|
||||||
|
import IconButton from '../../../../../common/components/buttons/IconButton';
|
||||||
|
import Input from '../../../../../common/components/input/input/Input';
|
||||||
import { isAlphanumeric } from '../../../../../common/utils/regex';
|
import { isAlphanumeric } from '../../../../../common/utils/regex';
|
||||||
|
|
||||||
|
import style from './GeneralPinInput.module.scss';
|
||||||
|
|
||||||
interface GeneralPinInputProps {
|
interface GeneralPinInputProps {
|
||||||
register: UseFormRegister<Settings>;
|
register: UseFormRegister<Settings>;
|
||||||
formName: keyof Settings;
|
formName: keyof Settings;
|
||||||
isDisabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function GeneralPinInput(props: PropsWithChildren<GeneralPinInputProps>) {
|
export default function GeneralPinInput({ register, formName, disabled }: PropsWithChildren<GeneralPinInputProps>) {
|
||||||
const { register, formName, isDisabled } = props;
|
|
||||||
const [isVisible, setVisible] = useState(false);
|
const [isVisible, setVisible] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputGroup size='sm' width='100px'>
|
<div className={style.container}>
|
||||||
<Input
|
<Input
|
||||||
variant='ontime-filled'
|
|
||||||
type={isVisible ? 'text' : 'password'}
|
type={isVisible ? 'text' : 'password'}
|
||||||
maxLength={4}
|
maxLength={4}
|
||||||
{...register(formName, {
|
{...register(formName, {
|
||||||
@@ -28,18 +30,16 @@ export default function GeneralPinInput(props: PropsWithChildren<GeneralPinInput
|
|||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
placeholder='-'
|
placeholder='-'
|
||||||
isDisabled={isDisabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
<InputRightElement>
|
<IconButton
|
||||||
<IconButton
|
onMouseDown={() => setVisible(true)}
|
||||||
onMouseDown={() => setVisible(true)}
|
onMouseUp={() => setVisible(false)}
|
||||||
onMouseUp={() => setVisible(false)}
|
variant='ghosted'
|
||||||
size='sm'
|
aria-label='Show pin code'
|
||||||
variant='ontime-ghosted'
|
>
|
||||||
icon={<IoEyeOutline />}
|
<IoEyeOutline />
|
||||||
aria-label='Show pin code'
|
</IconButton>
|
||||||
/>
|
</div>
|
||||||
</InputRightElement>
|
|
||||||
</InputGroup>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,8 @@ export default function QuickStart({ isOpen, onClose }: QuickStartProps) {
|
|||||||
error={errors.settings?.timeFormat?.message}
|
error={errors.settings?.timeFormat?.message}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
{...register('settings.timeFormat')}
|
value={watch('settings.timeFormat')}
|
||||||
|
onValueChange={(value: '12' | '24') => setValue('settings.timeFormat', value, { shouldDirty: true })}
|
||||||
defaultValue='24'
|
defaultValue='24'
|
||||||
options={[
|
options={[
|
||||||
{ value: '12', label: '12 hours 11:00:10 PM' },
|
{ value: '12', label: '12 hours 11:00:10 PM' },
|
||||||
@@ -90,7 +91,8 @@ export default function QuickStart({ isOpen, onClose }: QuickStartProps) {
|
|||||||
error={errors.settings?.language?.message}
|
error={errors.settings?.language?.message}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
{...register('settings.language')}
|
value={watch('settings.language')}
|
||||||
|
onValueChange={(value: string) => setValue('settings.language', value, { shouldDirty: true })}
|
||||||
defaultValue='en'
|
defaultValue='en'
|
||||||
options={[
|
options={[
|
||||||
{ value: 'en', label: 'English' },
|
{ value: 'en', label: 'English' },
|
||||||
@@ -132,6 +134,7 @@ export default function QuickStart({ isOpen, onClose }: QuickStartProps) {
|
|||||||
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
|
<Switch
|
||||||
|
size='large'
|
||||||
name='viewSettings.freezeEnd'
|
name='viewSettings.freezeEnd'
|
||||||
checked={watch('viewSettings.freezeEnd')}
|
checked={watch('viewSettings.freezeEnd')}
|
||||||
onCheckedChange={(checked) => setValue('viewSettings.freezeEnd', checked)}
|
onCheckedChange={(checked) => setValue('viewSettings.freezeEnd', checked)}
|
||||||
|
|||||||
@@ -2,16 +2,6 @@ $button-bg-gray: $gray-1050;
|
|||||||
$button-color-white: $gray-50;
|
$button-color-white: $gray-50;
|
||||||
|
|
||||||
@mixin tap-factory($theme-color) {
|
@mixin tap-factory($theme-color) {
|
||||||
font-family: $ontime-font-family;
|
|
||||||
font-size: calc(1rem + 2px);
|
|
||||||
border-radius: $component-border-radius-md;
|
|
||||||
width: 100%;
|
|
||||||
transition-property: color, background-color;
|
|
||||||
transition-duration: $transition-time-feedback;
|
|
||||||
display: grid;
|
|
||||||
place-content: center;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
|
|
||||||
background-color: $button-bg-gray;
|
background-color: $button-bg-gray;
|
||||||
color: $theme-color;
|
color: $theme-color;
|
||||||
|
|
||||||
@@ -43,6 +33,18 @@ $button-color-white: $gray-50;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tapButton {
|
||||||
|
font-family: $ontime-font-family;
|
||||||
|
font-size: calc(1rem + 2px);
|
||||||
|
border-radius: $component-border-radius-md;
|
||||||
|
width: 100%;
|
||||||
|
transition-property: color, background-color;
|
||||||
|
transition-duration: $transition-time-feedback;
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
.tapButton.neutral {
|
.tapButton.neutral {
|
||||||
@include tap-factory($gray-50);
|
@include tap-factory($gray-50);
|
||||||
|
|
||||||
@@ -86,7 +88,7 @@ $button-color-white: $gray-50;
|
|||||||
|
|
||||||
.tapButton.tight {
|
.tapButton.tight {
|
||||||
padding-inline: 0.5rem;
|
padding-inline: 0.5rem;
|
||||||
width: fit-content
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tapButton.fill {
|
.tapButton.fill {
|
||||||
|
|||||||
@@ -17,10 +17,15 @@ interface TapButtonProps {
|
|||||||
|
|
||||||
const TapButton = forwardRef((props: PropsWithChildren<TapButtonProps>, ref: ForwardedRef<HTMLButtonElement>) => {
|
const TapButton = forwardRef((props: PropsWithChildren<TapButtonProps>, ref: ForwardedRef<HTMLButtonElement>) => {
|
||||||
const { children, disabled, onClick, theme = 'neutral', aspect = 'normal', active, className } = props;
|
const { children, disabled, onClick, theme = 'neutral', aspect = 'normal', active, className } = props;
|
||||||
const classes = cx([style.tapButton, className, style[theme], style[aspect], active ? style.active : null]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className={classes} disabled={disabled} type='button' onClick={onClick} ref={ref}>
|
<button
|
||||||
|
className={cx([style.tapButton, className, style[theme], style[aspect], active && style.active])}
|
||||||
|
disabled={disabled}
|
||||||
|
type='button'
|
||||||
|
onClick={onClick}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ $info-hover: $section-white;
|
|||||||
user-select: text;
|
user-select: text;
|
||||||
min-height: 20vh;
|
min-height: 20vh;
|
||||||
max-height: 40vh;
|
max-height: 40vh;
|
||||||
|
background-color: $gray-1350;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.apart {
|
||||||
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logEntry {
|
.logEntry {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
import { IoClose } from 'react-icons/io5';
|
||||||
import { LogOrigin } from 'ontime-types';
|
import { LogOrigin } from 'ontime-types';
|
||||||
|
|
||||||
import Button from '../../common/components/buttons/Button';
|
import Button from '../../common/components/buttons/Button';
|
||||||
@@ -105,8 +106,8 @@ export default function Log() {
|
|||||||
>
|
>
|
||||||
{LogOrigin.Tx}
|
{LogOrigin.Tx}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant='subtle-destructive' size='small' onClick={clearLogs}>
|
<Button variant='subtle-destructive' size='small' onClick={clearLogs} className={style.apart}>
|
||||||
Clear
|
<IoClose /> Clear
|
||||||
</Button>
|
</Button>
|
||||||
</Panel.InlineElements>
|
</Panel.InlineElements>
|
||||||
<ul className={style.log}>
|
<ul className={style.log}>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
top: 10%;
|
top: 10%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
|
||||||
z-index: $zindex-dialog;
|
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
|
|
||||||
padding-inline: 1rem;
|
padding-inline: 1rem;
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ export default function EditModal(props: EditModalProps) {
|
|||||||
defaultValue={field.value}
|
defaultValue={field.value}
|
||||||
data-field={field.id}
|
data-field={field.id}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
resize='none'
|
|
||||||
rows={5}
|
rows={5}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ import {
|
|||||||
import { isStringBoolean } from '../viewers/common/viewUtils';
|
import { isStringBoolean } from '../viewers/common/viewUtils';
|
||||||
|
|
||||||
export const getOperatorOptions = (customFields: CustomFields, timeFormat: string): ViewOption[] => {
|
export const getOperatorOptions = (customFields: CustomFields, timeFormat: string): ViewOption[] => {
|
||||||
const fieldOptions = makeOptionsFromCustomFields(customFields, { title: 'Title', note: 'Note' });
|
const fieldOptions = makeOptionsFromCustomFields(customFields, [
|
||||||
|
{ value: 'title', label: 'Title' },
|
||||||
|
{ value: 'note', label: 'Note' },
|
||||||
|
]);
|
||||||
const customFieldSelect = makeCustomFieldSelectOptions(customFields);
|
const customFieldSelect = makeCustomFieldSelectOptions(customFields);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useSessionStorage } from '@mantine/hooks';
|
import { useSessionStorage } from '@mantine/hooks';
|
||||||
|
|
||||||
import { ContextMenu } from '../../common/components/context-menu/ContextMenu';
|
|
||||||
import { Corner } from '../../common/components/editor-utils/EditorUtils';
|
import { Corner } from '../../common/components/editor-utils/EditorUtils';
|
||||||
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
||||||
import ViewNavigationMenu from '../../common/components/navigation-menu/ViewNavigationMenu';
|
import ViewNavigationMenu from '../../common/components/navigation-menu/ViewNavigationMenu';
|
||||||
@@ -13,6 +12,7 @@ import { AppMode, sessionKeys } from '../../ontimeConfig';
|
|||||||
|
|
||||||
import RundownEntryEditor from './entry-editor/RundownEntryEditor';
|
import RundownEntryEditor from './entry-editor/RundownEntryEditor';
|
||||||
import FinderPlacement from './placements/FinderPlacement';
|
import FinderPlacement from './placements/FinderPlacement';
|
||||||
|
import { RundownContextMenu } from './rundown-context-menu/RundownContextMenu';
|
||||||
import RundownWrapper from './RundownWrapper';
|
import RundownWrapper from './RundownWrapper';
|
||||||
|
|
||||||
import style from './RundownExport.module.scss';
|
import style from './RundownExport.module.scss';
|
||||||
@@ -39,9 +39,9 @@ function RundownExport() {
|
|||||||
<ViewNavigationMenu suppressSettings />
|
<ViewNavigationMenu suppressSettings />
|
||||||
<div className={style.content}>
|
<div className={style.content}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ContextMenu>
|
<RundownContextMenu>
|
||||||
<RundownWrapper isSmallDevice />
|
<RundownWrapper isSmallDevice />
|
||||||
</ContextMenu>
|
</RundownContextMenu>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,9 +59,9 @@ function RundownExport() {
|
|||||||
<div className={style.list}>
|
<div className={style.list}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Corner onClick={(event) => handleLinks('rundown', event)} />
|
<Corner onClick={(event) => handleLinks('rundown', event)} />
|
||||||
<ContextMenu>
|
<RundownContextMenu>
|
||||||
<RundownWrapper />
|
<RundownWrapper />
|
||||||
</ContextMenu>
|
</RundownContextMenu>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
{!hideSideBar && (
|
{!hideSideBar && (
|
||||||
|
|||||||
@@ -16,12 +16,8 @@ function EventEditorFooter({ id, cue }: EventEditorFooterProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.footer}>
|
<div className={style.footer}>
|
||||||
<CopyTag copyValue={loadById} label='OSC trigger by ID'>
|
<CopyTag copyValue={loadById}>{loadById}</CopyTag>
|
||||||
{loadById}
|
<CopyTag copyValue={loadByCue}>{loadByCue}</CopyTag>
|
||||||
</CopyTag>
|
|
||||||
<CopyTag copyValue={loadByCue} label='OSC trigger by cue'>
|
|
||||||
{loadByCue}
|
|
||||||
</CopyTag>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { type CSSProperties, useCallback, useRef } from 'react';
|
import { type CSSProperties, useCallback, useRef } from 'react';
|
||||||
|
|
||||||
import * as Editor from '../../../../common/components/editor-utils/EditorUtils';
|
import * as Editor from '../../../../common/components/editor-utils/EditorUtils';
|
||||||
import { AutoTextArea } from '../../../../common/components/input/auto-text-area/AutoTextArea';
|
import { AutoTextarea } from '../../../../common/components/input/auto-textarea/AutoTextarea';
|
||||||
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
|
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
|
||||||
import { EventEditorUpdateFields } from '../EventEditor';
|
import { EventEditorUpdateFields } from '../EventEditor';
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ export default function EventTextArea({
|
|||||||
style: givenStyles,
|
style: givenStyles,
|
||||||
submitHandler,
|
submitHandler,
|
||||||
}: CountedTextAreaProps) {
|
}: CountedTextAreaProps) {
|
||||||
const ref = useRef<HTMLInputElement | null>(null);
|
const ref = useRef<HTMLTextAreaElement | null>(null);
|
||||||
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
|
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
|
||||||
|
|
||||||
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, {
|
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, {
|
||||||
@@ -34,14 +34,12 @@ export default function EventTextArea({
|
|||||||
<Editor.Label className={className} htmlFor={field} style={givenStyles}>
|
<Editor.Label className={className} htmlFor={field} style={givenStyles}>
|
||||||
{label}
|
{label}
|
||||||
</Editor.Label>
|
</Editor.Label>
|
||||||
<AutoTextArea
|
<AutoTextarea
|
||||||
id={field}
|
id={field}
|
||||||
inputref={ref}
|
inputref={ref}
|
||||||
rows={1}
|
rows={1}
|
||||||
size='sm'
|
|
||||||
resize='none'
|
|
||||||
variant='ontime-filled'
|
|
||||||
data-testid='input-textarea'
|
data-testid='input-textarea'
|
||||||
|
fluid
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
|
|||||||
@@ -67,10 +67,10 @@ function QuickAddInline({ previousEventId, parentBlock }: QuickAddInlineProps) {
|
|||||||
<div className={style.quickAdd} data-testid='quick-add-inline'>
|
<div className={style.quickAdd} data-testid='quick-add-inline'>
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
items={[
|
items={[
|
||||||
{ type: 'item', icon: <IoAdd />, label: 'Add Event', onClick: addEvent },
|
{ type: 'item', icon: IoAdd, label: 'Add Event', onClick: addEvent },
|
||||||
{ type: 'item', icon: <IoAdd />, label: 'Add Delay', onClick: addDelay },
|
{ type: 'item', icon: IoAdd, label: 'Add Delay', onClick: addDelay },
|
||||||
{ type: 'item', icon: <IoAdd />, label: 'Add Milestone', onClick: addMilestone },
|
{ type: 'item', icon: IoAdd, label: 'Add Milestone', onClick: addMilestone },
|
||||||
{ type: 'item', icon: <IoAdd />, label: 'Add Group', onClick: addBlock, disabled: parentBlock !== null },
|
{ type: 'item', icon: IoAdd, label: 'Add Group', onClick: addBlock, disabled: parentBlock !== null },
|
||||||
]}
|
]}
|
||||||
render={<IconButton size='small' variant='primary' className={style.addButton} />}
|
render={<IconButton size='small' variant='primary' className={style.addButton} />}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -36,21 +36,26 @@ export default function RundownBlock({ data, hasCursor, collapsed, onCollapse }:
|
|||||||
|
|
||||||
const [onContextMenu] = useContextMenu<HTMLDivElement>([
|
const [onContextMenu] = useContextMenu<HTMLDivElement>([
|
||||||
{
|
{
|
||||||
|
type: 'item',
|
||||||
label: 'Clone Group',
|
label: 'Clone Group',
|
||||||
icon: IoDuplicateOutline,
|
icon: IoDuplicateOutline,
|
||||||
onClick: () => clone(data.id),
|
onClick: () => clone(data.id),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
type: 'item',
|
||||||
|
|
||||||
label: 'Ungroup',
|
label: 'Ungroup',
|
||||||
icon: IoFolderOpenOutline,
|
icon: IoFolderOpenOutline,
|
||||||
onClick: () => ungroup(data.id),
|
onClick: () => ungroup(data.id),
|
||||||
isDisabled: data.entries.length === 0,
|
disabled: data.entries.length === 0,
|
||||||
},
|
},
|
||||||
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
|
type: 'item',
|
||||||
label: 'Delete Group',
|
label: 'Delete Group',
|
||||||
icon: IoTrash,
|
icon: IoTrash,
|
||||||
onClick: () => deleteEntry([data.id]),
|
onClick: () => deleteEntry([data.id]),
|
||||||
withDivider: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import type { PropsWithChildren } from 'react';
|
||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
import { DropdownMenuOption, PositionedDropdownMenu } from '../../../common/components/dropdown-menu/DropdownMenu';
|
||||||
|
|
||||||
|
type Position = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ContextMenuStore = {
|
||||||
|
position: Position;
|
||||||
|
options: DropdownMenuOption[];
|
||||||
|
isOpen: boolean;
|
||||||
|
setContextMenu: (position: Position, options: DropdownMenuOption[]) => void;
|
||||||
|
setIsOpen: (newIsOpen: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useContextMenuStore = create<ContextMenuStore>((set) => ({
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
options: [],
|
||||||
|
isOpen: false,
|
||||||
|
setContextMenu: (position, options) => set(() => ({ position, options, isOpen: true })),
|
||||||
|
setIsOpen: (newIsOpen) => set(() => ({ isOpen: newIsOpen })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export function RundownContextMenu({ children }: PropsWithChildren) {
|
||||||
|
const { position, options, isOpen, setIsOpen } = useContextMenuStore();
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
return setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
<PositionedDropdownMenu isOpen position={position} onClose={onClose} items={options} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -107,6 +107,7 @@ export default function RundownEvent({
|
|||||||
selectedEvents.size > 1
|
selectedEvents.size > 1
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
|
type: 'item',
|
||||||
label: 'Link to previous',
|
label: 'Link to previous',
|
||||||
icon: IoLink,
|
icon: IoLink,
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
@@ -116,6 +117,8 @@ export default function RundownEvent({
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
type: 'item',
|
||||||
|
|
||||||
label: 'Unlink from previous',
|
label: 'Unlink from previous',
|
||||||
icon: IoUnlink,
|
icon: IoUnlink,
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
@@ -124,11 +127,15 @@ export default function RundownEvent({
|
|||||||
value: null,
|
value: null,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{ withDivider: true, label: 'Group', icon: IoFolder, onClick: () => actionHandler('group') },
|
{ type: 'divider' },
|
||||||
{ withDivider: true, label: 'Delete', icon: IoTrash, onClick: () => actionHandler('delete') },
|
{ type: 'item', label: 'Group', icon: IoFolder, onClick: () => actionHandler('group') },
|
||||||
|
{ type: 'divider' },
|
||||||
|
{ type: 'item', label: 'Delete', icon: IoTrash, onClick: () => actionHandler('delete') },
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
|
type: 'item',
|
||||||
|
|
||||||
label: 'Toggle link to previous',
|
label: 'Toggle link to previous',
|
||||||
icon: IoLink,
|
icon: IoLink,
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
@@ -137,23 +144,34 @@ export default function RundownEvent({
|
|||||||
value: linkStart,
|
value: linkStart,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
{ type: 'divider' },
|
||||||
|
|
||||||
{
|
{
|
||||||
|
type: 'item',
|
||||||
|
|
||||||
label: 'Add to swap',
|
label: 'Add to swap',
|
||||||
icon: IoAdd,
|
icon: IoAdd,
|
||||||
onClick: () => setSelectedEventId(eventId),
|
onClick: () => setSelectedEventId(eventId),
|
||||||
withDivider: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
type: 'item',
|
||||||
|
|
||||||
label: `Swap this event with ${selectedEventId ?? ''}`,
|
label: `Swap this event with ${selectedEventId ?? ''}`,
|
||||||
icon: IoSwapVertical,
|
icon: IoSwapVertical,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
actionHandler('swap', { field: 'id', value: selectedEventId });
|
actionHandler('swap', { field: 'id', value: selectedEventId });
|
||||||
clearSelectedEventId();
|
clearSelectedEventId();
|
||||||
},
|
},
|
||||||
isDisabled: selectedEventId == null || selectedEventId === eventId,
|
disabled: selectedEventId == null || selectedEventId === eventId,
|
||||||
},
|
},
|
||||||
{ withDivider: false, label: 'Clone', icon: IoDuplicateOutline, onClick: () => actionHandler('clone') },
|
{
|
||||||
{ withDivider: true, label: 'Delete', icon: IoTrash, onClick: () => actionHandler('delete') },
|
type: 'item',
|
||||||
|
label: 'Clone',
|
||||||
|
icon: IoDuplicateOutline,
|
||||||
|
onClick: () => actionHandler('clone'),
|
||||||
|
},
|
||||||
|
{ type: 'divider' },
|
||||||
|
{ type: 'item', label: 'Delete', icon: IoTrash, onClick: () => actionHandler('delete') },
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,11 @@ export const getClockOptions = (timeFormat: string): ViewOption[] => [
|
|||||||
title: 'Align Horizontal',
|
title: 'Align Horizontal',
|
||||||
description: 'Moves the horizontally in page to start = left | center | end = right',
|
description: 'Moves the horizontally in page to start = left | center | end = right',
|
||||||
type: 'option',
|
type: 'option',
|
||||||
values: { start: 'Start', center: 'Center', end: 'End' },
|
values: [
|
||||||
|
{ value: 'start', label: 'Start' },
|
||||||
|
{ value: 'center', label: 'Center' },
|
||||||
|
{ value: 'end', label: 'End' },
|
||||||
|
],
|
||||||
defaultValue: 'center',
|
defaultValue: 'center',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -63,7 +67,11 @@ export const getClockOptions = (timeFormat: string): ViewOption[] => [
|
|||||||
title: 'Align Vertical',
|
title: 'Align Vertical',
|
||||||
description: 'Moves the vertically in page to start = left | center | end = right',
|
description: 'Moves the vertically in page to start = left | center | end = right',
|
||||||
type: 'option',
|
type: 'option',
|
||||||
values: { start: 'Start', center: 'Center', end: 'End' },
|
values: [
|
||||||
|
{ value: 'start', label: 'Start' },
|
||||||
|
{ value: 'center', label: 'Center' },
|
||||||
|
{ value: 'end', label: 'End' },
|
||||||
|
],
|
||||||
defaultValue: 'center',
|
defaultValue: 'center',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,16 +8,16 @@ import { makeOptionsFromCustomFields } from '../../../common/components/view-par
|
|||||||
import safeParseNumber from '../../../common/utils/safeParseNumber';
|
import safeParseNumber from '../../../common/utils/safeParseNumber';
|
||||||
|
|
||||||
export const getLowerThirdOptions = (customFields: CustomFields): ViewOption[] => {
|
export const getLowerThirdOptions = (customFields: CustomFields): ViewOption[] => {
|
||||||
const topSourceOptions = makeOptionsFromCustomFields(customFields, {
|
const topSourceOptions = makeOptionsFromCustomFields(customFields, [
|
||||||
title: 'Title',
|
{ value: 'title', label: 'Title' },
|
||||||
note: 'Note',
|
{ value: 'note', label: 'Note' },
|
||||||
});
|
]);
|
||||||
|
|
||||||
const bottomSourceOptions = makeOptionsFromCustomFields(customFields, {
|
const bottomSourceOptions = makeOptionsFromCustomFields(customFields, [
|
||||||
title: 'Title',
|
{ value: 'title', label: 'Title' },
|
||||||
note: 'Note',
|
{ value: 'note', label: 'Note' },
|
||||||
none: 'None',
|
{ value: 'none', label: 'None' },
|
||||||
});
|
]);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -99,10 +99,9 @@ export const getLowerThirdOptions = (customFields: CustomFields): ViewOption[] =
|
|||||||
{
|
{
|
||||||
id: 'width',
|
id: 'width',
|
||||||
title: 'Minimum Width',
|
title: 'Minimum Width',
|
||||||
description: 'Minimum Width of the element',
|
description: 'Minimum Width of the element (percentage)',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
prefix: '%',
|
placeholder: '45 (default %)',
|
||||||
placeholder: '45 (default)',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'key',
|
id: 'key',
|
||||||
|
|||||||
@@ -68,7 +68,11 @@ export const MINIMAL_TIMER_OPTIONS: ViewOption[] = [
|
|||||||
title: 'Align Horizontal',
|
title: 'Align Horizontal',
|
||||||
description: 'Moves the horizontally in page to start = left | center | end = right',
|
description: 'Moves the horizontally in page to start = left | center | end = right',
|
||||||
type: 'option',
|
type: 'option',
|
||||||
values: { start: 'Start', center: 'Center', end: 'End' },
|
values: [
|
||||||
|
{ value: 'start', label: 'Start' },
|
||||||
|
{ value: 'center', label: 'Center' },
|
||||||
|
{ value: 'end', label: 'End' },
|
||||||
|
],
|
||||||
defaultValue: 'center',
|
defaultValue: 'center',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -83,7 +87,11 @@ export const MINIMAL_TIMER_OPTIONS: ViewOption[] = [
|
|||||||
title: 'Align Vertical',
|
title: 'Align Vertical',
|
||||||
description: 'Moves the vertically in page to start = left | center | end = right',
|
description: 'Moves the vertically in page to start = left | center | end = right',
|
||||||
type: 'option',
|
type: 'option',
|
||||||
values: { start: 'Start', center: 'Center', end: 'End' },
|
values: [
|
||||||
|
{ value: 'start', label: 'Start' },
|
||||||
|
{ value: 'center', label: 'Center' },
|
||||||
|
{ value: 'end', label: 'End' },
|
||||||
|
],
|
||||||
defaultValue: 'center',
|
defaultValue: 'center',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+59
-17
@@ -43,24 +43,29 @@ $track-color: $white-1;
|
|||||||
$thumb-color: $gray-1100;
|
$thumb-color: $gray-1100;
|
||||||
$thumb-color-hover: $gray-900;
|
$thumb-color-hover: $gray-900;
|
||||||
|
|
||||||
* {
|
/* Apply a natural box layout model to all elements */
|
||||||
|
html {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: inherit;
|
||||||
|
margin: 0;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
scrollbar-color: $thumb-color $track-color;
|
scrollbar-color: $thumb-color $track-color;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset box-sizing
|
body,
|
||||||
*,
|
html {
|
||||||
*:before,
|
|
||||||
*:after {
|
|
||||||
box-sizing: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
body, html {
|
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-family: $ontime-font-family;
|
font-family: $ontime-font-family;
|
||||||
background-color: var(--background-color-override, $ui-black);
|
background-color: var(--background-color-override, $ui-black);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
body,
|
body,
|
||||||
@@ -76,6 +81,51 @@ html,
|
|||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reset button styles
|
||||||
|
button {
|
||||||
|
all: unset;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
picture,
|
||||||
|
video,
|
||||||
|
canvas,
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
input,
|
||||||
|
button,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
p,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove buttons in number inputs
|
||||||
|
/* WebKit and Blink */
|
||||||
|
input::-webkit-outer-spin-button,
|
||||||
|
input::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
/* Firefox */
|
||||||
|
input[type='number'] {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
/* smaller root size for MacOS laptops */
|
/* smaller root size for MacOS laptops */
|
||||||
@media (max-width: 1600px) {
|
@media (max-width: 1600px) {
|
||||||
body,
|
body,
|
||||||
@@ -85,14 +135,6 @@ html,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* workaround for chakra
|
|
||||||
* https://github.com/chakra-ui/chakra-ui/issues/417
|
|
||||||
*/
|
|
||||||
option {
|
|
||||||
color: initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* width */
|
/* width */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
export const ontimeAlertOnDark = {
|
|
||||||
container: {
|
|
||||||
fontSize: 'calc(1rem - 1px)',
|
|
||||||
backgroundColor: '#202020', // $gray-1200
|
|
||||||
color: '#e2e2e2', // $gray-200
|
|
||||||
borderRadius: '3px',
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
alignSelf: 'start',
|
|
||||||
color: '#578AF4', // $blue-500
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ontimeDialog = {
|
|
||||||
container: {
|
|
||||||
backgroundColor: '#1a1a1a', // $gray-1300
|
|
||||||
color: '#e2e2e2', // $gray-200
|
|
||||||
borderRadius: '3px',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
export const ontimeButtonFilled = {
|
|
||||||
background: '#2B5ABC', // $blue-700
|
|
||||||
color: '#fff', // pure-white
|
|
||||||
border: '1px solid #2B5ABC', // $blue-700
|
|
||||||
_hover: {
|
|
||||||
backgroundColor: '#0A43B9', // $blue-800
|
|
||||||
border: '1px solid #0A43B9', // $blue-800
|
|
||||||
_disabled: {
|
|
||||||
background: '#2B5ABC', // $blue-700
|
|
||||||
},
|
|
||||||
},
|
|
||||||
_active: {
|
|
||||||
backgroundColor: '#0036A6', // blue-900
|
|
||||||
borderColor: '#002A90', // blue-1000
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ontimeButtonOutlined = {
|
|
||||||
backgroundColor: '#2d2d2d', // $gray-1100
|
|
||||||
color: '#e2e2e2', // $blue-400
|
|
||||||
border: '1px solid rgba(255, 255, 255, 0.10)', // white-10
|
|
||||||
_hover: {
|
|
||||||
backgroundColor: '#404040', // $gray-1000
|
|
||||||
_disabled: {
|
|
||||||
backgroundColor: '#2d2d2d', // $gray-1100
|
|
||||||
},
|
|
||||||
},
|
|
||||||
_active: {
|
|
||||||
backgroundColor: '#2d2d2d', // $gray-1100
|
|
||||||
borderColor: '#202020', // $gray-1250
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ontimeButtonSubtle = {
|
|
||||||
backgroundColor: '#303030', // $gray-1050
|
|
||||||
color: '#779BE7', // $blue-400
|
|
||||||
border: '1px solid transparent',
|
|
||||||
_hover: {
|
|
||||||
background: '#404040', // $gray-1000
|
|
||||||
_disabled: {
|
|
||||||
backgroundColor: '#303030', // $gray-1050
|
|
||||||
},
|
|
||||||
},
|
|
||||||
_active: {
|
|
||||||
backgroundColor: '#2d2d2d', // $gray-1100
|
|
||||||
borderColor: '#202020', // $gray-1250
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: revise colours
|
|
||||||
export const ontimeButtonGhostedWhite = {
|
|
||||||
...ontimeButtonSubtle,
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
color: 'white',
|
|
||||||
_hover: {
|
|
||||||
background: '#404040', // $gray-1000
|
|
||||||
},
|
|
||||||
_active: {
|
|
||||||
background: '#2d2d2d', // $gray-1100
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ontimeButtonGhosted = {
|
|
||||||
...ontimeButtonSubtle,
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
_hover: {
|
|
||||||
background: '#404040', // $gray-1000
|
|
||||||
_disabled: {
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ontimeButtonSubtleWhite = {
|
|
||||||
...ontimeButtonSubtle,
|
|
||||||
color: '#f6f6f6', // $gray-50
|
|
||||||
fontWeight: 600,
|
|
||||||
};
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
export const ontimeCheckboxOnDark = {
|
|
||||||
control: {
|
|
||||||
border: '1px',
|
|
||||||
borderColor: '#2d2d2d', // $gray-1100
|
|
||||||
backgroundColor: '#2d2d2d', // $gray-1100
|
|
||||||
_disabled: {
|
|
||||||
color: 'white',
|
|
||||||
borderColor: '#2d2d2d', // $gray-1100
|
|
||||||
backgroundColor: '#2d2d2d', // $gray-1100
|
|
||||||
opacity: 0.6,
|
|
||||||
},
|
|
||||||
_checked: {
|
|
||||||
borderColor: '#578AF4', // $blue-500
|
|
||||||
backgroundColor: '#578AF4', // $blue-500
|
|
||||||
_disabled: {
|
|
||||||
color: 'white',
|
|
||||||
borderColor: '#578AF4', // $blue-500
|
|
||||||
backgroundColor: '#578AF4', // $blue-500
|
|
||||||
opacity: 0.6,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
_focus: {
|
|
||||||
boxShadow: 'none',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
fontWeight: '200',
|
|
||||||
color: '#9d9d9d', // $gray-500
|
|
||||||
_checked: {
|
|
||||||
color: '#cfcfcf', // $gray-300
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
export const ontimeDrawer = {
|
|
||||||
header: {
|
|
||||||
color: '#fefefe', // $gray-50
|
|
||||||
backgroundColor: '#202020', // $gray-1250
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
color: '#fefefe', // $gray-50
|
|
||||||
backgroundColor: '#202020', // $gray-1250
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
backgroundColor: '#202020', // $gray-1250
|
|
||||||
},
|
|
||||||
closeButton: {
|
|
||||||
color: '#fefefe', // $gray-50
|
|
||||||
_hover: {
|
|
||||||
color: '#303030', // $gray-1050
|
|
||||||
backgroundColor: '#fefefe', // $gray-50
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
export const ontimeEditable = {
|
|
||||||
input: {
|
|
||||||
borderRadius: '3px',
|
|
||||||
width: '100%',
|
|
||||||
_focus: {
|
|
||||||
border: '1px solid #578AF4', // $blue-500
|
|
||||||
boxShadow: 'none',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
preview: {
|
|
||||||
width: '100%',
|
|
||||||
border: '1px solid transparent', // $blue-500
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
export const ontimeMenuOnDark = {
|
|
||||||
list: {
|
|
||||||
fontSize: 'calc(1rem - 2px)',
|
|
||||||
borderRadius: '3px',
|
|
||||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
color: '#ececec', // $gray-1030
|
|
||||||
backgroundColor: '#202020', // $gray-1250
|
|
||||||
zIndex: 100,
|
|
||||||
},
|
|
||||||
item: {
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
paddingBlock: '0.5rem',
|
|
||||||
_hover: {
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.2)',
|
|
||||||
_disabled: {
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
_disabled: {
|
|
||||||
color: '#b1b1b1', // $gray-400
|
|
||||||
},
|
|
||||||
},
|
|
||||||
divider: {
|
|
||||||
borderColor: 'rgba(255, 255, 255, 0.07)',
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
export const ontimeModal = {
|
|
||||||
header: {
|
|
||||||
fontWeight: 400,
|
|
||||||
letterSpacing: '0.3px',
|
|
||||||
padding: '1rem 1.5rem',
|
|
||||||
fontSize: '1.25rem',
|
|
||||||
color: '#fefefe', // $gray-50
|
|
||||||
},
|
|
||||||
dialog: {
|
|
||||||
borderRadius: '3px',
|
|
||||||
padding: 0,
|
|
||||||
minHeight: 'min(200px, 10vh)',
|
|
||||||
backgroundColor: '#202020', // $gray-1250
|
|
||||||
color: '#fefefe', // $gray-50
|
|
||||||
border: '1px solid #2d2d2d', // $gray-1100
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
padding: '1rem',
|
|
||||||
fontSize: 'calc(1rem - 2px)',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: '1rem',
|
|
||||||
},
|
|
||||||
closeButton: {
|
|
||||||
color: '#fefefe', // $gray-50
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
padding: '1rem',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'left',
|
|
||||||
gap: '0.5rem',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
export const ontimeRadio = {
|
|
||||||
control: {
|
|
||||||
borderColor: '#262626', // $gray-1200
|
|
||||||
backgroundColor: '#262626', // $gray-1200
|
|
||||||
_checked: {
|
|
||||||
borderColor: '#262626', // $gray-1200
|
|
||||||
color: '#f6f6f6', // $ui-white
|
|
||||||
backgroundColor: '#f6f6f6', // $ui-white
|
|
||||||
},
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
color: '#9d9d9d', // $gray-500, same as placeholder value
|
|
||||||
_checked: {
|
|
||||||
color: '#f6f6f6', // $gray-200
|
|
||||||
},
|
|
||||||
_hover: {
|
|
||||||
color: '#e2e2e2', // $gray-200
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ontimeBlockRadio = {
|
|
||||||
control: {
|
|
||||||
borderColor: '#262626', // $gray-1200
|
|
||||||
backgroundColor: '#262626', // $gray-1200
|
|
||||||
_checked: {
|
|
||||||
borderColor: '#262626', // $gray-1200
|
|
||||||
color: '#578AF4', // $blue-500
|
|
||||||
backgroundColor: '#578AF4', // $blue-500
|
|
||||||
},
|
|
||||||
_hover: {
|
|
||||||
color: '#578AF4', // $blue-500
|
|
||||||
backgroundColor: '#578AF4', // $blue-500
|
|
||||||
outline: 'none',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
fontSize: '0.7em',
|
|
||||||
letterSpacing: '0.3px',
|
|
||||||
color: '#9d9d9d', // $gray-500
|
|
||||||
_checked: {
|
|
||||||
color: '#cfcfcf', // $gray-300
|
|
||||||
},
|
|
||||||
_hover: {
|
|
||||||
color: '#e2e2e2', // $gray-200
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
export const ontimeSelect = {
|
|
||||||
field: {
|
|
||||||
color: '#e2e2e2', // $gray-200
|
|
||||||
borderRadius: '3px',
|
|
||||||
fontWeight: '400',
|
|
||||||
background: '#262626', // $gray-1100
|
|
||||||
border: '1px solid transparent',
|
|
||||||
_hover: {
|
|
||||||
background: '#404040', // $gray-1000
|
|
||||||
},
|
|
||||||
_focus: {
|
|
||||||
background: '#404040', // $gray-1000
|
|
||||||
color: '#f6f6f6', // $gray-50
|
|
||||||
border: '1px solid #578AF4', // $blue-500
|
|
||||||
},
|
|
||||||
_disabled: {
|
|
||||||
_hover: {
|
|
||||||
background: '#262626', // $gray-1100
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
color: '#e2e2e2', // $gray-200
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
export const ontimeSwitch = {
|
|
||||||
track: {
|
|
||||||
background: '#2d2d2d', // $gray-1100
|
|
||||||
border: '1px solid transparent',
|
|
||||||
_checked: {
|
|
||||||
background: '#2B5ABC', // $blue-700
|
|
||||||
},
|
|
||||||
_focus: {
|
|
||||||
border: '1px solid #578AF4', // $blue-500
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
export const ontimeTab = {
|
|
||||||
tab: {
|
|
||||||
fontWeight: 600,
|
|
||||||
borderBottom: '2px solid transparent',
|
|
||||||
color: '#9d9d9d', // $gray-500
|
|
||||||
marginBottom: '-2px',
|
|
||||||
_selected: {
|
|
||||||
color: '#101010', // $ui-black
|
|
||||||
border: 'none',
|
|
||||||
borderBottom: '2px solid #779BE7', // $blue-400
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tablist: {
|
|
||||||
borderBottom: '2px solid #ececec', // $gray-100
|
|
||||||
},
|
|
||||||
tabpanel: {
|
|
||||||
padding: 0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
const commonStyles = {
|
|
||||||
fontWeight: '400',
|
|
||||||
backgroundColor: '#262626', // $gray-1200
|
|
||||||
color: '#e2e2e2', // $gray-200
|
|
||||||
border: '1px solid transparent',
|
|
||||||
borderRadius: '3px',
|
|
||||||
_hover: {
|
|
||||||
backgroundColor: '#2d2d2d', // $gray-1100
|
|
||||||
},
|
|
||||||
_focus: {
|
|
||||||
backgroundColor: '#2d2d2d', // $gray-1000
|
|
||||||
color: '#f6f6f6', // $gray-50
|
|
||||||
border: '1px solid #578AF4', // $blue-500
|
|
||||||
},
|
|
||||||
_placeholder: { color: '#9d9d9d' }, // $gray-500
|
|
||||||
_disabled: {
|
|
||||||
_hover: {
|
|
||||||
backgroundColor: '#262626', // $gray-1200
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ontimeInputFilled = {
|
|
||||||
field: {
|
|
||||||
...commonStyles,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ontimeInputGhosted = {
|
|
||||||
field: {
|
|
||||||
...commonStyles,
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
color: '#f6f6f6', // $gray-50
|
|
||||||
_hover: {
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
border: '1px solid #2B5ABC', // $blue-500
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ontimeInputTransparent = {
|
|
||||||
field: {
|
|
||||||
...commonStyles,
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
_hover: {
|
|
||||||
backgroundColor: '#2d2d2d', // $gray-1100
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ontimeTextAreaFilled = {
|
|
||||||
...commonStyles,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ontimeTextAreaTransparent = {
|
|
||||||
...commonStyles,
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
_hover: {
|
|
||||||
backgroundColor: '#2d2d2d', // $gray-1100
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
export const ontimeTooltip = {
|
|
||||||
_light: {
|
|
||||||
backgroundColor: '#2d2d2d', // $gray-1100
|
|
||||||
color: '#ececec', // $gray-100
|
|
||||||
padding: '2px 8px',
|
|
||||||
},
|
|
||||||
_dark: {
|
|
||||||
backgroundColor: '#2d2d2d', // $gray-1100
|
|
||||||
color: '#ececec', // $gray-100
|
|
||||||
padding: '2px 8px',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
import { extendTheme } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
import { ontimeAlertOnDark, ontimeDialog } from './OntimeAlert';
|
|
||||||
import {
|
|
||||||
ontimeButtonFilled,
|
|
||||||
ontimeButtonGhosted,
|
|
||||||
ontimeButtonGhostedWhite,
|
|
||||||
ontimeButtonOutlined,
|
|
||||||
ontimeButtonSubtle,
|
|
||||||
ontimeButtonSubtleWhite,
|
|
||||||
} from './ontimeButton';
|
|
||||||
import { ontimeCheckboxOnDark } from './ontimeCheckbox';
|
|
||||||
import { ontimeDrawer } from './ontimeDrawer';
|
|
||||||
import { ontimeEditable } from './ontimeEditable';
|
|
||||||
import { ontimeMenuOnDark } from './ontimeMenu';
|
|
||||||
import { ontimeModal } from './ontimeModal';
|
|
||||||
import { ontimeBlockRadio, ontimeRadio } from './ontimeRadio';
|
|
||||||
import { ontimeSelect } from './ontimeSelect';
|
|
||||||
import { ontimeSwitch } from './ontimeSwitch';
|
|
||||||
import { ontimeTab } from './ontimeTab';
|
|
||||||
import {
|
|
||||||
ontimeInputFilled,
|
|
||||||
ontimeInputGhosted,
|
|
||||||
ontimeInputTransparent,
|
|
||||||
ontimeTextAreaFilled,
|
|
||||||
ontimeTextAreaTransparent,
|
|
||||||
} from './ontimeTextInputs';
|
|
||||||
import { ontimeTooltip } from './ontimeTooltip';
|
|
||||||
|
|
||||||
const theme = extendTheme({
|
|
||||||
initialColorMode: 'dark',
|
|
||||||
useSystemColorMode: false,
|
|
||||||
components: {
|
|
||||||
Alert: {
|
|
||||||
variants: {
|
|
||||||
'ontime-on-dark-info': { ...ontimeAlertOnDark },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
AlertDialog: {
|
|
||||||
variants: {
|
|
||||||
ontime: { ...ontimeDialog },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Button: {
|
|
||||||
baseStyle: {
|
|
||||||
letterSpacing: '0.3px',
|
|
||||||
fontWeight: '400',
|
|
||||||
borderRadius: '3px',
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
'ontime-filled': { ...ontimeButtonFilled },
|
|
||||||
'ontime-outlined': { ...ontimeButtonOutlined },
|
|
||||||
'ontime-subtle': { ...ontimeButtonSubtle },
|
|
||||||
'ontime-ghosted': { ...ontimeButtonGhosted },
|
|
||||||
'ontime-ghosted-white': { ...ontimeButtonGhostedWhite },
|
|
||||||
'ontime-subtle-white': { ...ontimeButtonSubtleWhite },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Checkbox: {
|
|
||||||
variants: {
|
|
||||||
'ontime-ondark': { ...ontimeCheckboxOnDark },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Drawer: {
|
|
||||||
variants: {
|
|
||||||
ontime: { ...ontimeDrawer },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Editable: {
|
|
||||||
variants: {
|
|
||||||
ontime: { ...ontimeEditable },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Input: {
|
|
||||||
baseStyle: {
|
|
||||||
borderRadius: '3px',
|
|
||||||
border: '1px',
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
'ontime-filled': { ...ontimeInputFilled },
|
|
||||||
'ontime-ghosted': { ...ontimeInputGhosted },
|
|
||||||
'ontime-transparent': { ...ontimeInputTransparent },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Kbd: {
|
|
||||||
baseStyle: {
|
|
||||||
borderRadius: '2px',
|
|
||||||
border: 'none',
|
|
||||||
background: '#262626', // $gray-1200
|
|
||||||
padding: '0.125rem 0.5rem',
|
|
||||||
color: '#f6f6f6', // $ui-white
|
|
||||||
fontWeight: 400,
|
|
||||||
boxShadow: '0px 0px 3px 0px rgba(0,0,0,0.4)',
|
|
||||||
fontSize: 'calc(1rem - 2px)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Menu: {
|
|
||||||
variants: {
|
|
||||||
'ontime-on-dark': { ...ontimeMenuOnDark },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Modal: {
|
|
||||||
baseStyle: {
|
|
||||||
background: 'rgba(0, 0, 0, 0.5)',
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
ontime: { ...ontimeModal },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Radio: {
|
|
||||||
variants: {
|
|
||||||
ontime: { ...ontimeRadio },
|
|
||||||
'ontime-block': { ...ontimeBlockRadio },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Tabs: {
|
|
||||||
variants: {
|
|
||||||
ontime: { ...ontimeTab },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Textarea: {
|
|
||||||
baseStyle: {
|
|
||||||
borderRadius: '3px',
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
'ontime-filled': { ...ontimeTextAreaFilled },
|
|
||||||
'ontime-transparent': { ...ontimeTextAreaTransparent },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Tooltip: {
|
|
||||||
baseStyle: { ...ontimeTooltip },
|
|
||||||
},
|
|
||||||
Switch: {
|
|
||||||
variants: {
|
|
||||||
ontime: { ...ontimeSwitch },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Select: {
|
|
||||||
variants: {
|
|
||||||
ontime: { ...ontimeSelect },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default theme;
|
|
||||||
@@ -9,7 +9,7 @@ import { makeOptionsFromCustomFields } from '../../common/components/view-params
|
|||||||
import { scheduleOptions } from '../common/schedule/schedule.options';
|
import { scheduleOptions } from '../common/schedule/schedule.options';
|
||||||
|
|
||||||
export const getBackstageOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => {
|
export const getBackstageOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => {
|
||||||
const secondaryOptions = makeOptionsFromCustomFields(customFields, { note: 'Note' });
|
const secondaryOptions = makeOptionsFromCustomFields(customFields, [{ value: 'note', label: 'Note' }]);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] },
|
{ title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] },
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const getCountdownOptions = (
|
|||||||
customFields: CustomFields,
|
customFields: CustomFields,
|
||||||
persistedSubscriptions: EntryId[],
|
persistedSubscriptions: EntryId[],
|
||||||
): ViewOption[] => {
|
): ViewOption[] => {
|
||||||
const secondaryOptions = makeOptionsFromCustomFields(customFields, { note: 'Note' });
|
const secondaryOptions = makeOptionsFromCustomFields(customFields, [{ value: 'note', label: 'Note' }]);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] },
|
{ title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] },
|
||||||
|
|||||||
+5
-12
@@ -1,6 +1,6 @@
|
|||||||
import { memo, useCallback, useRef } from 'react';
|
import { memo, useCallback, useRef } from 'react';
|
||||||
|
|
||||||
import { AutoTextArea } from '../../../../common/components/input/auto-text-area/AutoTextArea';
|
import { AutoTextarea } from '../../../../common/components/input/auto-textarea/AutoTextarea';
|
||||||
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
|
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
|
||||||
|
|
||||||
interface MultiLineCellProps {
|
interface MultiLineCellProps {
|
||||||
@@ -11,7 +11,7 @@ interface MultiLineCellProps {
|
|||||||
export default memo(MultiLineCell);
|
export default memo(MultiLineCell);
|
||||||
|
|
||||||
function MultiLineCell({ initialValue, handleUpdate }: MultiLineCellProps) {
|
function MultiLineCell({ initialValue, handleUpdate }: MultiLineCellProps) {
|
||||||
const ref = useRef<HTMLInputElement | null>(null);
|
const ref = useRef<HTMLTextAreaElement | null>(null);
|
||||||
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
|
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
|
||||||
|
|
||||||
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, {
|
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, {
|
||||||
@@ -20,18 +20,11 @@ function MultiLineCell({ initialValue, handleUpdate }: MultiLineCellProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AutoTextArea
|
<AutoTextarea
|
||||||
inputref={ref}
|
inputref={ref}
|
||||||
|
variant='ghosted'
|
||||||
|
fluid
|
||||||
rows={1}
|
rows={1}
|
||||||
size='sm'
|
|
||||||
style={{
|
|
||||||
minHeight: '2rem',
|
|
||||||
padding: '0',
|
|
||||||
paddingTop: '0.25rem',
|
|
||||||
fontSize: '1rem',
|
|
||||||
}}
|
|
||||||
transition='none'
|
|
||||||
variant='ontime-transparent'
|
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
|
|||||||
+7
-7
@@ -24,46 +24,46 @@ function CuesheetTableMenu() {
|
|||||||
isOpen
|
isOpen
|
||||||
onClose={closeMenu}
|
onClose={closeMenu}
|
||||||
items={[
|
items={[
|
||||||
{ type: 'item', label: 'Edit...', onClick: () => showModal(entryId), icon: <IoOptions /> },
|
{ type: 'item', label: 'Edit...', onClick: () => showModal(entryId), icon: IoOptions },
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
type: 'item',
|
type: 'item',
|
||||||
label: 'Add event above',
|
label: 'Add event above',
|
||||||
onClick: () => addEntry({ type: SupportedEntry.Event, parent: parentId }, { before: entryId }),
|
onClick: () => addEntry({ type: SupportedEntry.Event, parent: parentId }, { before: entryId }),
|
||||||
icon: <IoAdd />,
|
icon: IoAdd,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'item',
|
type: 'item',
|
||||||
label: 'Add event below',
|
label: 'Add event below',
|
||||||
onClick: () => addEntry({ type: SupportedEntry.Event, parent: parentId }, { after: entryId }),
|
onClick: () => addEntry({ type: SupportedEntry.Event, parent: parentId }, { after: entryId }),
|
||||||
icon: <IoAdd />,
|
icon: IoAdd,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'item',
|
type: 'item',
|
||||||
label: 'Clone event',
|
label: 'Clone event',
|
||||||
onClick: () => clone(entryId),
|
onClick: () => clone(entryId),
|
||||||
icon: <IoDuplicateOutline />,
|
icon: IoDuplicateOutline,
|
||||||
},
|
},
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
type: 'item',
|
type: 'item',
|
||||||
label: 'Move up',
|
label: 'Move up',
|
||||||
onClick: () => move(entryId, 'up'),
|
onClick: () => move(entryId, 'up'),
|
||||||
icon: <IoArrowUp />,
|
icon: IoArrowUp,
|
||||||
disabled: entryIndex < 1,
|
disabled: entryIndex < 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'item',
|
type: 'item',
|
||||||
label: 'Move down',
|
label: 'Move down',
|
||||||
onClick: () => move(entryId, 'down'),
|
onClick: () => move(entryId, 'down'),
|
||||||
icon: <IoArrowDown />,
|
icon: IoArrowDown,
|
||||||
},
|
},
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
type: 'item',
|
type: 'item',
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
onClick: () => deleteEntry([entryId]),
|
onClick: () => deleteEntry([entryId]),
|
||||||
icon: <IoTrash />,
|
icon: IoTrash,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
position={position}
|
position={position}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useSearchParams } from 'react-router-dom';
|
|||||||
import { CustomFields, OntimeEvent, TimerType } from 'ontime-types';
|
import { CustomFields, OntimeEvent, TimerType } from 'ontime-types';
|
||||||
import { validateTimerType } from 'ontime-utils';
|
import { validateTimerType } from 'ontime-utils';
|
||||||
|
|
||||||
|
import type { SelectOption } from '../../common/components/select/Select';
|
||||||
import {
|
import {
|
||||||
getTimeOption,
|
getTimeOption,
|
||||||
hideTimerSeconds,
|
hideTimerSeconds,
|
||||||
@@ -14,16 +15,22 @@ import { makeOptionsFromCustomFields } from '../../common/components/view-params
|
|||||||
import { isStringBoolean } from '../../features/viewers/common/viewUtils';
|
import { isStringBoolean } from '../../features/viewers/common/viewUtils';
|
||||||
|
|
||||||
// manually match the properties of TimerType excluding the None
|
// manually match the properties of TimerType excluding the None
|
||||||
const timerDisplayOptions = {
|
const timerDisplayOptions: SelectOption[] = [
|
||||||
'no-overrides': 'No Overrides',
|
{ value: 'no-overrides', label: 'No Overrides' },
|
||||||
'count-up': 'Count up',
|
{ value: TimerType.CountUp, label: 'Count Up' },
|
||||||
'count-down': 'Count down',
|
{ value: TimerType.CountDown, label: 'Count Down' },
|
||||||
clock: 'Clock',
|
{ value: TimerType.Clock, label: 'Clock' },
|
||||||
};
|
];
|
||||||
|
|
||||||
export const getTimerOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => {
|
export const getTimerOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => {
|
||||||
const mainOptions = makeOptionsFromCustomFields(customFields, { title: 'Title', note: 'Note' });
|
const mainOptions = makeOptionsFromCustomFields(customFields, [
|
||||||
const secondaryOptions = makeOptionsFromCustomFields(customFields, { title: 'Title', note: 'Note' });
|
{ value: 'title', label: 'Title' },
|
||||||
|
{ value: 'note', label: 'Note' },
|
||||||
|
]);
|
||||||
|
const secondaryOptions = makeOptionsFromCustomFields(customFields, [
|
||||||
|
{ value: 'title', label: 'Title' },
|
||||||
|
{ value: 'note', label: 'Note' },
|
||||||
|
]);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] },
|
{ title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] },
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ test('sheet file upload', async ({ page }) => {
|
|||||||
const fileChooser = await fileChooserPromise;
|
const fileChooser = await fileChooserPromise;
|
||||||
await fileChooser.setFiles(fileToUpload);
|
await fileChooser.setFiles(fileToUpload);
|
||||||
|
|
||||||
await page.locator('[id="event\\ schedule"]').selectOption('Sheet2');
|
await page.getByRole('row', { name: 'Worksheet' }).getByRole('combobox').click();
|
||||||
await page.locator('[id="event\\ schedule"]').selectOption('test');
|
await page.getByRole('option', { name: 'test' }).click();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Import preview' }).click();
|
await page.getByRole('button', { name: 'Import preview' }).click();
|
||||||
await page.getByRole('button', { name: 'Apply' }).click();
|
await page.getByRole('button', { name: 'Apply' }).click();
|
||||||
|
|||||||
Generated
+5
-575
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user