diff --git a/apps/client/src/common/components/view-params-editor/ViewParamShare.tsx b/apps/client/src/common/components/view-params-editor/ViewParamShare.tsx new file mode 100644 index 000000000..c4488eedb --- /dev/null +++ b/apps/client/src/common/components/view-params-editor/ViewParamShare.tsx @@ -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 ( +
+ {viewPresets.map((preset) => { + const active = window.location.search.includes(`alias=${preset.alias}`); + return ( +
+
{preset.alias}
+ +
+ ); + })} +
+ ); +} diff --git a/apps/client/src/common/components/view-params-editor/ViewParamsEditor.tsx b/apps/client/src/common/components/view-params-editor/ViewParamsEditor.tsx index a51e80310..23ccf1ac6 100644 --- a/apps/client/src/common/components/view-params-editor/ViewParamsEditor.tsx +++ b/apps/client/src/common/components/view-params-editor/ViewParamsEditor.tsx @@ -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) => { @@ -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 && ( This view style is being modified by a custom CSS file. )} +
{viewOptions.map((section) => ( diff --git a/apps/client/src/common/components/view-params-editor/ViewParamsSection.module.scss b/apps/client/src/common/components/view-params-editor/ViewParamsSection.module.scss index f58b04a74..6e00b2818 100644 --- a/apps/client/src/common/components/view-params-editor/ViewParamsSection.module.scss +++ b/apps/client/src/common/components/view-params-editor/ViewParamsSection.module.scss @@ -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; } diff --git a/apps/client/src/common/components/view-params-editor/ViewParamsSection.tsx b/apps/client/src/common/components/view-params-editor/ViewParamsSection.tsx index 9d9dc4fac..275f95bb0 100644 --- a/apps/client/src/common/components/view-params-editor/ViewParamsSection.tsx +++ b/apps/client/src/common/components/view-params-editor/ViewParamsSection.tsx @@ -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 = () => { diff --git a/apps/client/src/common/components/view-params-editor/ViewParamsShare.module.scss b/apps/client/src/common/components/view-params-editor/ViewParamsShare.module.scss new file mode 100644 index 000000000..94a2fd88c --- /dev/null +++ b/apps/client/src/common/components/view-params-editor/ViewParamsShare.module.scss @@ -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; +} diff --git a/apps/client/src/common/hooks-query/useUrlPresets.ts b/apps/client/src/common/hooks-query/useUrlPresets.ts index 4d4667bb4..bc48aaa29 100644 --- a/apps/client/src/common/hooks-query/useUrlPresets.ts +++ b/apps/client/src/common/hooks-query/useUrlPresets.ts @@ -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(); diff --git a/apps/client/src/common/utils/urlPresets.ts b/apps/client/src/common/utils/urlPresets.ts index de4502491..3e6cd9a46 100644 --- a/apps/client/src/common/utils/urlPresets.ts +++ b/apps/client/src/common/utils/urlPresets.ts @@ -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}`; } diff --git a/apps/client/src/features/app-settings/panel/feature-panel/composite/URLPresetForm.tsx b/apps/client/src/features/app-settings/panel/feature-panel/composite/URLPresetForm.tsx index b35cc6726..5d7ac1369 100644 --- a/apps/client/src/features/app-settings/panel/feature-panel/composite/URLPresetForm.tsx +++ b/apps/client/src/features/app-settings/panel/feature-panel/composite/URLPresetForm.tsx @@ -115,7 +115,7 @@ export default function URLPresetForm({ urlPreset, onClose }: URLPresetFormProps
Generate options (paste URL to generate options) - +
diff --git a/apps/client/src/features/operator/Operator.tsx b/apps/client/src/features/operator/Operator.tsx index b6fa66cb7..75fcc7a9a 100644 --- a/apps/client/src/features/operator/Operator.tsx +++ b/apps/client/src/features/operator/Operator.tsx @@ -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 (
- + {editEvent && setEditEvent(null)} />} diff --git a/apps/client/src/views/backstage/Backstage.tsx b/apps/client/src/views/backstage/Backstage.tsx index f1cb64d1f..fe64eeaa5 100644 --- a/apps/client/src/views/backstage/Backstage.tsx +++ b/apps/client/src/views/backstage/Backstage.tsx @@ -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 (
- +
{general?.logo && }
{general.title}
diff --git a/apps/client/src/views/countdown/Countdown.tsx b/apps/client/src/views/countdown/Countdown.tsx index b06aab754..a1b83410b 100644 --- a/apps/client/src/views/countdown/Countdown.tsx +++ b/apps/client/src/views/countdown/Countdown.tsx @@ -6,6 +6,7 @@ import { isOntimeEvent, isPlayableEvent, OntimeEvent, + OntimeView, ProjectData, Settings, } from 'ontime-types'; @@ -70,7 +71,7 @@ export default function Countdown({ return (
- +
{general?.logo && }
{general.title}
diff --git a/apps/client/src/views/project-info/ProjectInfo.tsx b/apps/client/src/views/project-info/ProjectInfo.tsx index cb89bf6a9..f3b4b67f9 100644 --- a/apps/client/src/views/project-info/ProjectInfo.tsx +++ b/apps/client/src/views/project-info/ProjectInfo.tsx @@ -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 ( <> - + ; ); @@ -38,7 +39,7 @@ export default function ProjectInfo() { return ( <> - + ; ); @@ -47,7 +48,7 @@ export default function ProjectInfo() { return (
- + {data.logo && }
{data.title && ( diff --git a/apps/client/src/views/studio/Studio.tsx b/apps/client/src/views/studio/Studio.tsx index cb94c5d6a..9463ad8da 100644 --- a/apps/client/src/views/studio/Studio.tsx +++ b/apps/client/src/views/studio/Studio.tsx @@ -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 (
- +
{general?.logo && } diff --git a/apps/client/src/views/timeline/TimelinePage.tsx b/apps/client/src/views/timeline/TimelinePage.tsx index 85beb974c..e1f5c9f24 100644 --- a/apps/client/src/views/timeline/TimelinePage.tsx +++ b/apps/client/src/views/timeline/TimelinePage.tsx @@ -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 (
- +
{general?.logo && }
{general.title}
diff --git a/apps/client/src/views/timer/Timer.tsx b/apps/client/src/views/timer/Timer.tsx index ba7660209..e3141744c 100644 --- a/apps/client/src/views/timer/Timer.tsx +++ b/apps/client/src/views/timer/Timer.tsx @@ -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 && } - +
diff --git a/e2e/tests/features/207-view-params.spec.ts b/e2e/tests/features/207-view-params.spec.ts index f44d19b0f..3f2a54570 100644 --- a/e2e/tests/features/207-view-params.spec.ts +++ b/e2e/tests/features/207-view-params.spec.ts @@ -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/);