mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-11 18:33:53 +00:00
feat: expose presets in view options
This commit is contained in:
committed by
Carlos Valente
parent
5f8a5c4995
commit
307f0ffd34
@@ -0,0 +1,45 @@
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { OntimeView, URLPreset } from 'ontime-types';
|
||||
|
||||
import { useViewUrlPresets } from '../../hooks-query/useUrlPresets';
|
||||
import { cx } from '../../utils/styleUtils';
|
||||
import Button from '../buttons/Button';
|
||||
|
||||
import style from './ViewParamsShare.module.scss';
|
||||
|
||||
/**
|
||||
* Shows a list of presets for the current view
|
||||
*/
|
||||
export function ViewParamsShare({ target }: { target: OntimeView }) {
|
||||
const { viewPresets } = useViewUrlPresets(target);
|
||||
const [_, setSearchParams] = useSearchParams();
|
||||
|
||||
const handleRecall = (preset: URLPreset) => {
|
||||
setSearchParams(`${preset.search}&alias=${preset.alias}`);
|
||||
};
|
||||
|
||||
if (viewPresets.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={style.presetSection}>
|
||||
{viewPresets.map((preset) => {
|
||||
const active = window.location.search.includes(`alias=${preset.alias}`);
|
||||
return (
|
||||
<div key={preset.alias} className={cx([style.preset, active && style.active])}>
|
||||
<div>{preset.alias}</div>
|
||||
<Button
|
||||
variant='subtle-white'
|
||||
onClick={() => handleRecall(preset)}
|
||||
disabled={active}
|
||||
className={style.presetActions}
|
||||
>
|
||||
{active ? 'Applied' : 'Apply'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { FormEvent, memo } from 'react';
|
||||
import { FormEvent, memo, useReducer } from 'react';
|
||||
import { IoClose } from 'react-icons/io5';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { Dialog } from '@base-ui-components/react/dialog';
|
||||
import { OntimeView } from 'ontime-types';
|
||||
|
||||
import useViewSettings from '../../hooks-query/useViewSettings';
|
||||
import Button from '../buttons/Button';
|
||||
@@ -11,20 +12,23 @@ import Info from '../info/Info';
|
||||
import { ViewOption } from './viewParams.types';
|
||||
import { getURLSearchParamsFromObj } from './viewParams.utils';
|
||||
import { useViewParamsEditorStore } from './viewParamsEditor.store';
|
||||
import { ViewParamsShare } from './ViewParamShare';
|
||||
import ViewParamsSection from './ViewParamsSection';
|
||||
|
||||
import style from './ViewParamsEditor.module.scss';
|
||||
|
||||
interface EditFormDrawerProps {
|
||||
target: OntimeView;
|
||||
viewOptions: ViewOption[];
|
||||
}
|
||||
|
||||
export default memo(ViewParamsEditor);
|
||||
|
||||
function ViewParamsEditor({ viewOptions }: EditFormDrawerProps) {
|
||||
function ViewParamsEditor({ target, viewOptions }: EditFormDrawerProps) {
|
||||
const [_, setSearchParams] = useSearchParams();
|
||||
const { data: viewSettings } = useViewSettings();
|
||||
const { isOpen, close } = useViewParamsEditorStore();
|
||||
// TODO: we dont want this as a permanent option
|
||||
const forceRender = useReducer((x) => x + 1, 0)[1];
|
||||
|
||||
const handleClose = () => {
|
||||
close();
|
||||
@@ -32,6 +36,7 @@ function ViewParamsEditor({ viewOptions }: EditFormDrawerProps) {
|
||||
|
||||
const resetParams = () => {
|
||||
setSearchParams();
|
||||
forceRender();
|
||||
};
|
||||
|
||||
const onParamsFormSubmit = (formEvent: FormEvent<HTMLFormElement>) => {
|
||||
@@ -40,6 +45,7 @@ function ViewParamsEditor({ viewOptions }: EditFormDrawerProps) {
|
||||
const newParamsObject = Object.fromEntries(new FormData(formEvent.currentTarget));
|
||||
const newSearchParams = getURLSearchParamsFromObj(newParamsObject, viewOptions);
|
||||
setSearchParams(newSearchParams);
|
||||
forceRender();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -64,6 +70,7 @@ function ViewParamsEditor({ viewOptions }: EditFormDrawerProps) {
|
||||
{viewSettings.overrideStyles && (
|
||||
<Info className={style.info}>This view style is being modified by a custom CSS file.</Info>
|
||||
)}
|
||||
<ViewParamsShare target={target} />
|
||||
<form id='edit-params-form' onSubmit={onParamsFormSubmit} className={style.sectionList}>
|
||||
{viewOptions.map((section) => (
|
||||
<ViewParamsSection
|
||||
@@ -80,7 +87,7 @@ function ViewParamsEditor({ viewOptions }: EditFormDrawerProps) {
|
||||
Reset to default
|
||||
</Button>
|
||||
<Button variant='primary' size='large' form='edit-params-form' type='submit'>
|
||||
Save
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Popup>
|
||||
|
||||
@@ -36,11 +36,11 @@
|
||||
}
|
||||
|
||||
.closed {
|
||||
transition: rotate $transition-time-feedback;
|
||||
transition: rotate 300ms cubic-bezier(0.45, 1.005, 0, 1.005);
|
||||
rotate: 0deg;
|
||||
}
|
||||
|
||||
.open {
|
||||
transition: rotate $transition-time-feedback;
|
||||
transition: rotate 300ms cubic-bezier(0.45, 1.005, 0, 1.005);
|
||||
rotate: 180deg;
|
||||
}
|
||||
|
||||
@@ -15,8 +15,7 @@ interface ViewParamsSectionProps {
|
||||
options: ParamField[];
|
||||
}
|
||||
|
||||
export default memo(ViewParamsSection);
|
||||
function ViewParamsSection({ title, collapsible, options }: ViewParamsSectionProps) {
|
||||
export default function ViewParamsSection({ title, collapsible, options }: ViewParamsSectionProps) {
|
||||
const [collapsed, setCollapsed] = useLocalStorage({ key: `params-${title}`, defaultValue: false });
|
||||
|
||||
const handleCollapse = () => {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
.presetSection {
|
||||
background-color: $gray-1350;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
padding: 1rem 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.preset {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
&.active {
|
||||
color: $blue-500;
|
||||
}
|
||||
}
|
||||
|
||||
.presetActions {
|
||||
margin-left: auto;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { URLPreset } from 'ontime-types';
|
||||
import { OntimeView, URLPreset } from 'ontime-types';
|
||||
|
||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { URL_PRESETS } from '../api/constants';
|
||||
@@ -21,6 +22,14 @@ export default function useUrlPresets({ skip = false }: FetchProps = {}) {
|
||||
return { data: data ?? [], status, isError, refetch };
|
||||
}
|
||||
|
||||
export function useViewUrlPresets(view: OntimeView) {
|
||||
const { data } = useUrlPresets();
|
||||
|
||||
const viewPresets = useMemo(() => data.filter((preset) => preset.target === view), [data, view]);
|
||||
|
||||
return { viewPresets };
|
||||
}
|
||||
|
||||
export function useUpdateUrlPreset() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
@@ -136,9 +136,9 @@ export function arePathsEquivalent(currentPath: string, newPath: string): boolea
|
||||
* Generates a URL preset from a user given alias and URL.
|
||||
*/
|
||||
export function generateUrlPresetOptions(alias: string, userUrl: string): URLPreset {
|
||||
let sanitisedUrl = userUrl.toLowerCase();
|
||||
let sanitisedUrl = userUrl;
|
||||
// we need to ensure the URL has a protocol, but it doesnt matter which
|
||||
if (!checkRegex.startsWithHttp(sanitisedUrl)) {
|
||||
if (!checkRegex.startsWithHttp(userUrl.toLowerCase())) {
|
||||
sanitisedUrl = `http://${sanitisedUrl}`;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -115,7 +115,7 @@ export default function URLPresetForm({ urlPreset, onClose }: URLPresetFormProps
|
||||
<div className={style.expand}>
|
||||
<Panel.Description>Generate options (paste URL to generate options)</Panel.Description>
|
||||
<Panel.InlineElements>
|
||||
<Input placeholder='Paste URL' fluid ref={urlRef} required />
|
||||
<Input placeholder='Paste URL' fluid ref={urlRef} />
|
||||
<Button onClick={generateOptions}>Generate</Button>
|
||||
</Panel.InlineElements>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { isOntimeBlock, isOntimeEvent } from 'ontime-types';
|
||||
import { isOntimeBlock, isOntimeEvent, OntimeView } from 'ontime-types';
|
||||
|
||||
import EmptyPage from '../../common/components/state/EmptyPage';
|
||||
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
||||
@@ -116,7 +116,7 @@ export default function Operator() {
|
||||
|
||||
return (
|
||||
<div className={style.operatorContainer} data-testid='operator-view'>
|
||||
<ViewParamsEditor viewOptions={operatorOptions} />
|
||||
<ViewParamsEditor target={OntimeView.Operator} viewOptions={operatorOptions} />
|
||||
{editEvent && <EditModal event={editEvent} onClose={() => setEditEvent(null)} />}
|
||||
|
||||
<StatusBar />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import QRCode from 'react-qr-code';
|
||||
import { useViewportSize } from '@mantine/hooks';
|
||||
import { CustomFields, OntimeEvent, ProjectData, Runtime, Settings } from 'ontime-types';
|
||||
import { CustomFields, OntimeEvent, OntimeView, ProjectData, Runtime, Settings } from 'ontime-types';
|
||||
import { millisToString, removeLeadingZero } from 'ontime-utils';
|
||||
|
||||
import ProgressBar from '../../common/components/progress-bar/ProgressBar';
|
||||
@@ -110,7 +110,7 @@ export default function Backstage({
|
||||
|
||||
return (
|
||||
<div className={`backstage ${isMirrored ? 'mirror' : ''}`} data-testid='backstage-view'>
|
||||
<ViewParamsEditor viewOptions={backstageOptions} />
|
||||
<ViewParamsEditor target={OntimeView.Backstage} viewOptions={backstageOptions} />
|
||||
<div className='project-header'>
|
||||
{general?.logo && <ViewLogo name={general.logo} className='logo' />}
|
||||
<div className='title'>{general.title}</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
isOntimeEvent,
|
||||
isPlayableEvent,
|
||||
OntimeEvent,
|
||||
OntimeView,
|
||||
ProjectData,
|
||||
Settings,
|
||||
} from 'ontime-types';
|
||||
@@ -70,7 +71,7 @@ export default function Countdown({
|
||||
|
||||
return (
|
||||
<div className={`countdown ${isMirrored ? 'mirror' : ''}`} data-testid='countdown-view'>
|
||||
<ViewParamsEditor viewOptions={countdownOptions} />
|
||||
<ViewParamsEditor target={OntimeView.Countdown} viewOptions={countdownOptions} />
|
||||
<div className='project-header'>
|
||||
{general?.logo && <ViewLogo name={general.logo} className='logo' />}
|
||||
<div className='title'>{general.title}</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useViewOptionsStore } from '../../common/stores/viewOptions';
|
||||
import { useTranslation } from '../../translation/TranslationProvider';
|
||||
|
||||
import './ProjectInfo.scss';
|
||||
import { OntimeView } from 'ontime-types';
|
||||
|
||||
export default function ProjectInfo() {
|
||||
// persisted app state
|
||||
@@ -23,7 +24,7 @@ export default function ProjectInfo() {
|
||||
return (
|
||||
<>
|
||||
<ViewNavigationMenu isLockable suppressSettings />
|
||||
<ViewParamsEditor viewOptions={[]} />
|
||||
<ViewParamsEditor target={OntimeView.ProjectInfo} viewOptions={[]} />
|
||||
<EmptyPage text={getLocalizedString('common.no_data')} />;
|
||||
</>
|
||||
);
|
||||
@@ -38,7 +39,7 @@ export default function ProjectInfo() {
|
||||
return (
|
||||
<>
|
||||
<ViewNavigationMenu isLockable suppressSettings />
|
||||
<ViewParamsEditor viewOptions={[]} />
|
||||
<ViewParamsEditor target={OntimeView.ProjectInfo} viewOptions={[]} />
|
||||
<EmptyPage text={getLocalizedString('common.no_data')} />;
|
||||
</>
|
||||
);
|
||||
@@ -47,7 +48,7 @@ export default function ProjectInfo() {
|
||||
return (
|
||||
<div className={`project ${isMirrored ? 'mirror' : ''}`} data-testid='project-view'>
|
||||
<ViewNavigationMenu isLockable suppressSettings />
|
||||
<ViewParamsEditor viewOptions={[]} />
|
||||
<ViewParamsEditor target={OntimeView.ProjectInfo} viewOptions={[]} />
|
||||
{data.logo && <ViewLogo name={data.logo} className='logo' />}
|
||||
<div className='info'>
|
||||
{data.title && (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { MessageState, OntimeEvent, ProjectData, Runtime, Settings, ViewSettings } from 'ontime-types';
|
||||
import { MessageState, OntimeEvent, OntimeView, ProjectData, Runtime, Settings, ViewSettings } from 'ontime-types';
|
||||
|
||||
import ViewLogo from '../../common/components/view-logo/ViewLogo';
|
||||
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
||||
@@ -48,7 +48,7 @@ export default function Studio({
|
||||
|
||||
return (
|
||||
<div className={cx(['studio', isMirrored && 'mirror'])} data-testid='studio-view'>
|
||||
<ViewParamsEditor viewOptions={studioOptions} />
|
||||
<ViewParamsEditor target={OntimeView.StudioClock} viewOptions={studioOptions} />
|
||||
|
||||
<div className='project-header'>
|
||||
{general?.logo && <ViewLogo name={general.logo} className='logo' />}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { MaybeString, OntimeEvent, ProjectData, Runtime, Settings } from 'ontime-types';
|
||||
import { MaybeString, OntimeEvent, OntimeView, ProjectData, Runtime, Settings } from 'ontime-types';
|
||||
|
||||
import ViewLogo from '../../common/components/view-logo/ViewLogo';
|
||||
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
||||
@@ -72,7 +72,7 @@ export default function TimelinePage({ events, general, runtime, selectedId, set
|
||||
}
|
||||
return (
|
||||
<div className='timeline' data-testid='timeline-view'>
|
||||
<ViewParamsEditor viewOptions={progressOptions} />
|
||||
<ViewParamsEditor target={OntimeView.Timeline} viewOptions={progressOptions} />
|
||||
<div className='project-header'>
|
||||
{general?.logo && <ViewLogo name={general.logo} className='logo' />}
|
||||
<div className='title'>{general.title}</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { CustomFields, MessageState, OntimeEvent, ProjectData, Settings, ViewSettings } from 'ontime-types';
|
||||
import { CustomFields, MessageState, OntimeEvent, OntimeView, ProjectData, Settings, ViewSettings } from 'ontime-types';
|
||||
|
||||
import { FitText } from '../../common/components/fit-text/FitText';
|
||||
import MultiPartProgressBar from '../../common/components/multi-part-progress-bar/MultiPartProgressBar';
|
||||
@@ -157,7 +157,7 @@ export default function Timer({
|
||||
>
|
||||
{!hideLogo && general?.logo && <ViewLogo name={general.logo} className='logo' />}
|
||||
|
||||
<ViewParamsEditor viewOptions={timerOptions} />
|
||||
<ViewParamsEditor target={OntimeView.Timer} viewOptions={timerOptions} />
|
||||
|
||||
<div className={cx(['blackout', message.timer.blackout && 'blackout--active'])} />
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ test('View params configures timer view', async ({ page }) => {
|
||||
await page.mouse.move(Math.random() * 100, Math.random() * 100);
|
||||
await page.getByTestId('navigation__toggle-settings').click();
|
||||
await page.locator('label').filter({ hasText: 'Hide Time NowHides the Time' }).locator('span').nth(2).click();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('button', { name: 'Apply' }).click();
|
||||
|
||||
await expect(page.getByText('TIME NOW', { exact: true })).not.toBeInViewport();
|
||||
await expect(page).toHaveURL(/.*hideClock=true/);
|
||||
|
||||
Reference in New Issue
Block a user