diff --git a/apps/client/src/common/components/navigation-menu/NavigationMenu.tsx b/apps/client/src/common/components/navigation-menu/NavigationMenu.tsx index 765cfc2ef..3663f64fc 100644 --- a/apps/client/src/common/components/navigation-menu/NavigationMenu.tsx +++ b/apps/client/src/common/components/navigation-menu/NavigationMenu.tsx @@ -8,6 +8,7 @@ import { useLocation } from 'react-router'; import { isLocalhost, supportsFullscreen } from '../../../externals'; import { canUseWakeLock, useKeepAwakeOptions } from '../../../features/keep-awake/useWakeLock'; import { navigatorConstants } from '../../../viewerConfig'; +import useUrlPresets from '../../hooks-query/useUrlPresets'; import { useIsSmallScreen } from '../../hooks/useIsSmallScreen'; import { useClientStore } from '../../stores/clientStore'; import { useViewOptionsStore } from '../../stores/viewOptions'; @@ -105,6 +106,8 @@ function NavigationMenu({ isOpen, onClose }: NavigationMenuProps) { {route.label} ))} + + {isLocalhost && ( @@ -117,3 +120,27 @@ function NavigationMenu({ isOpen, onClose }: NavigationMenuProps) { ); } + +function PresetNavigation({ isSmallScreen, onClose }: { isSmallScreen: boolean; onClose: () => void }) { + const location = useLocation(); + const { data: urlPresets } = useUrlPresets(); + const navPresets = urlPresets.filter((preset) => preset.enabled && preset.displayInNav); + + if (navPresets.length === 0) return null; + + return ( + <> +
+ {navPresets.map((preset) => ( + + {preset.alias} + + ))} + + ); +} diff --git a/apps/client/src/common/utils/__tests__/urlPresets.test.ts b/apps/client/src/common/utils/__tests__/urlPresets.test.ts index 21880b55f..c7d674f49 100644 --- a/apps/client/src/common/utils/__tests__/urlPresets.test.ts +++ b/apps/client/src/common/utils/__tests__/urlPresets.test.ts @@ -37,6 +37,7 @@ describe('getRouteFromPreset()', () => { alias: 'demopage', target: OntimeView.Timer, search: 'user=guest', + displayInNav: false, options: {}, }, ]; @@ -119,6 +120,7 @@ describe('getRouteFromPreset()', () => { alias: 'cuesheet-4685d6', target: OntimeView.Cuesheet, search: '', + displayInNav: false, options: { read: 'full', write: '-', @@ -131,6 +133,7 @@ describe('getRouteFromPreset()', () => { alias: 'cuesheet-basic', target: OntimeView.Cuesheet, search: '', + displayInNav: false, }, ]; const cuesheetPresetWithNavLock: URLPreset[] = [ @@ -139,6 +142,7 @@ describe('getRouteFromPreset()', () => { alias: 'cuesheet-locked', target: OntimeView.Cuesheet, search: 'n=1', + displayInNav: false, }, ]; @@ -220,6 +224,7 @@ describe('generateUrlPresetOptions', () => { target: 'timer', search: 'param1=value1¶m2=value2', enabled: true, + displayInNav: false, }, ], [ @@ -231,6 +236,7 @@ describe('generateUrlPresetOptions', () => { target: 'timer', search: 'param1=value1¶m2=value2', enabled: true, + displayInNav: false, }, ], [ @@ -242,6 +248,7 @@ describe('generateUrlPresetOptions', () => { target: 'timer', search: 'param1=value1¶m2=value2', enabled: true, + displayInNav: false, }, ], [ @@ -253,6 +260,7 @@ describe('generateUrlPresetOptions', () => { target: 'timer', search: 'param1=value1¶m2=value2', enabled: true, + displayInNav: false, }, ], ])('should generate URL preset options for %s', (_description, alias, url, expected) => { diff --git a/apps/client/src/common/utils/urlPresets.ts b/apps/client/src/common/utils/urlPresets.ts index d153888e0..b28a3f971 100644 --- a/apps/client/src/common/utils/urlPresets.ts +++ b/apps/client/src/common/utils/urlPresets.ts @@ -194,6 +194,7 @@ export function generateUrlPresetOptions(alias: string, userUrl: string): URLPre target: path, search: url.searchParams.toString(), enabled: true, + displayInNav: false, }; } diff --git a/apps/client/src/features/app-settings/panel/feature-panel/URLPresets.tsx b/apps/client/src/features/app-settings/panel/feature-panel/URLPresets.tsx index 1c4d0674f..26e6f0d83 100644 --- a/apps/client/src/features/app-settings/panel/feature-panel/URLPresets.tsx +++ b/apps/client/src/features/app-settings/panel/feature-panel/URLPresets.tsx @@ -2,6 +2,7 @@ import { URLPreset } from 'ontime-types'; import { useState } from 'react'; import { IoAdd, IoOpenOutline, IoPencil, IoTrash } from 'react-icons/io5'; +import { maybeAxiosError } from '../../../../common/api/utils'; import Button from '../../../../common/components/buttons/Button'; import IconButton from '../../../../common/components/buttons/IconButton'; import Info from '../../../../common/components/info/Info'; @@ -22,13 +23,23 @@ const urlPresetsDocs = 'https://docs.getontime.no/features/url-presets/'; export default function URLPresets() { const [formState, setFormState] = useState({ isOpen: false, preset: undefined }); + const [actionError, setActionError] = useState(null); const { data, status } = useUrlPresets(); - const { deletePreset, isMutating } = useUpdateUrlPreset(); + const { updatePreset, deletePreset, isMutating } = useUpdateUrlPreset(); const openNewForm = () => setFormState({ isOpen: true }); const openEditForm = (preset: URLPreset) => setFormState({ isOpen: true, preset }); const closeForm = () => setFormState({ isOpen: false, preset: undefined }); + const persistPreset = async (preset: URLPreset) => { + setActionError(null); + try { + await updatePreset(preset.alias, preset); + } catch (error) { + setActionError(maybeAxiosError(error)); + } + }; + return ( @@ -53,10 +64,12 @@ export default function URLPresets() { {formState.isOpen && } + {actionError && {actionError}} Enabled + Show in nav Target view Alias @@ -68,7 +81,18 @@ export default function URLPresets() { return ( - {}} /> + persistPreset({ ...preset, enabled: checked })} + disabled={isMutating} + /> + + + persistPreset({ ...preset, displayInNav: checked })} + disabled={isMutating} + /> {preset.target} 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 d2fcebd9a..faa264e31 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 @@ -32,6 +32,7 @@ const defaultValues: URLPreset = { target: OntimeView.Timer, search: '', enabled: true, + displayInNav: false, }; interface URLPresetFormProps { diff --git a/apps/client/src/features/sharing/GenerateLinkForm.tsx b/apps/client/src/features/sharing/GenerateLinkForm.tsx index 283694678..f6fa07f23 100644 --- a/apps/client/src/features/sharing/GenerateLinkForm.tsx +++ b/apps/client/src/features/sharing/GenerateLinkForm.tsx @@ -95,6 +95,7 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, presets, is enabled: true, alias, search: '', + displayInNav: false, options: { read: options.read, write: options.write, diff --git a/apps/client/src/views/cuesheet/__tests__/cuesheet.policies.test.ts b/apps/client/src/views/cuesheet/__tests__/cuesheet.policies.test.ts index 2444460f9..da5767a70 100644 --- a/apps/client/src/views/cuesheet/__tests__/cuesheet.policies.test.ts +++ b/apps/client/src/views/cuesheet/__tests__/cuesheet.policies.test.ts @@ -20,6 +20,7 @@ describe('getCuesheetPermissionsPolicy()', () => { alias: 'cuesheet-read-only', target: OntimeView.Cuesheet, search: '', + displayInNav: false, options: { read: 'full', write: '-', @@ -41,6 +42,7 @@ describe('getCuesheetPermissionsPolicy()', () => { alias: 'cuesheet-flag', target: OntimeView.Cuesheet, search: '', + displayInNav: false, options: { read: 'full', write: 'flag', @@ -62,6 +64,7 @@ describe('getCuesheetPermissionsPolicy()', () => { alias: 'cuesheet-default', target: OntimeView.Cuesheet, search: '', + displayInNav: false, }; const policy = getCuesheetColumnAccessPolicy(preset, AppMode.Edit); @@ -76,6 +79,7 @@ describe('getCuesheetPermissionsPolicy()', () => { alias: 'cuesheet-granular', target: OntimeView.Cuesheet, search: '', + displayInNav: false, options: { read: 'cue,title', write: 'title', diff --git a/apps/server/src/api-data/db/migration/db.migration.v3.ts b/apps/server/src/api-data/db/migration/db.migration.v3.ts index 4a54cf685..4f8d43794 100644 --- a/apps/server/src/api-data/db/migration/db.migration.v3.ts +++ b/apps/server/src/api-data/db/migration/db.migration.v3.ts @@ -110,14 +110,13 @@ type old_URLPreset = { /** * migrates a url presets from v3 to v4 * - pathAndParams split into a target and search - * */ export function migrateURLPresets(jsonData: object): URLPreset[] | undefined { if (is.objectWithKeys(jsonData, ['urlPresets']) && is.array(jsonData.urlPresets)) { const oldURLPresets = structuredClone(jsonData.urlPresets) as old_URLPreset; const newURLPreset: URLPreset[] = oldURLPresets.map(({ enabled, alias, pathAndParams }) => { const [target, search] = pathAndParams.split('?'); - return { enabled, alias, target, search, options: {} } as URLPreset; + return { enabled, alias, target, search, displayInNav: false, options: {} } as URLPreset; }); return newURLPreset; } diff --git a/apps/server/src/api-data/db/migration/migration.test.ts b/apps/server/src/api-data/db/migration/migration.test.ts index 6a958c20e..77c1b775b 100644 --- a/apps/server/src/api-data/db/migration/migration.test.ts +++ b/apps/server/src/api-data/db/migration/migration.test.ts @@ -208,6 +208,7 @@ describe('v3 to v4', () => { target: OntimeView.Timer, search: 'showLeadingZeros=true&timerType=clock&hideClock=true&hideCards=true&hideProgress=true&hideMessage=true&hideSecondary=true&hideLogo=true', + displayInNav: false, options: {}, }, { @@ -216,6 +217,7 @@ describe('v3 to v4', () => { target: OntimeView.Timer, search: 'showLeadingZeros=true&hideClock=true&hideCards=true&hideProgress=true&hideMessage=true&hideSecondary=true&hideLogo=true', + displayInNav: false, options: {}, }, ]; diff --git a/apps/server/src/api-data/url-presets/__tests__/urlPresets.parser.test.ts b/apps/server/src/api-data/url-presets/__tests__/urlPresets.parser.test.ts index bc3184ea6..9040c8207 100644 --- a/apps/server/src/api-data/url-presets/__tests__/urlPresets.parser.test.ts +++ b/apps/server/src/api-data/url-presets/__tests__/urlPresets.parser.test.ts @@ -12,7 +12,9 @@ describe('parseUrlPresets()', () => { it('parses data, skipping invalid results', () => { const errorEmitter = vi.fn(); - const urlPresets = [{ enabled: true, alias: 'alias', target: 'timer', search: 'ss' }] as URLPreset[]; + const urlPresets = [ + { enabled: true, alias: 'alias', target: 'timer', search: 'ss', displayInNav: false }, + ] as URLPreset[]; const result = parseUrlPresets({ urlPresets }, errorEmitter); expect(result.length).toEqual(1); expect(result.at(0)).toMatchObject({ @@ -36,6 +38,7 @@ describe('parseUrlPresets()', () => { alias: 'testalias', target: 'timer', search: 'testpathAndParams', + displayInNav: false, }, ], } as unknown as DatabaseModel; diff --git a/apps/server/src/api-data/url-presets/urlPresets.parser.ts b/apps/server/src/api-data/url-presets/urlPresets.parser.ts index 3e2c1fd57..d96cababa 100644 --- a/apps/server/src/api-data/url-presets/urlPresets.parser.ts +++ b/apps/server/src/api-data/url-presets/urlPresets.parser.ts @@ -26,6 +26,7 @@ export function parseUrlPresets(data: Partial, emitError?: ErrorE alias: preset.alias, target: preset.target, search: preset.search ?? '', + displayInNav: preset.displayInNav ?? false, options: preset?.options, }; newPresets.push(newPreset); diff --git a/apps/server/src/api-data/url-presets/urlPresets.router.ts b/apps/server/src/api-data/url-presets/urlPresets.router.ts index 81d3ac7f1..5faaaa453 100644 --- a/apps/server/src/api-data/url-presets/urlPresets.router.ts +++ b/apps/server/src/api-data/url-presets/urlPresets.router.ts @@ -21,6 +21,7 @@ router.post('/', validateNewPreset, async (req: Request, res: Response { it('should merge the urlPresets key when present', () => { const newData = { urlPresets: [ - { enabled: true, alias: 'alias1', search: '' }, - { enabled: true, alias: 'alias2', search: '' }, + { enabled: true, alias: 'alias1', target: 'timer', search: '', displayInNav: false }, + { enabled: true, alias: 'alias2', target: 'timer', search: '', displayInNav: false }, ] as URLPreset[], }; diff --git a/apps/server/src/models/demoProject.ts b/apps/server/src/models/demoProject.ts index 05f90bf71..db21d6075 100644 --- a/apps/server/src/models/demoProject.ts +++ b/apps/server/src/models/demoProject.ts @@ -43,6 +43,7 @@ export const demoDb: DatabaseModel = { target: OntimeView.Timer, search: 'showLeadingZeros=true&timerType=clock&hideClock=true&hideCards=true&hideProgress=true&hideMessage=true&hideSecondary=true&hideLogo=true', + displayInNav: false, }, { enabled: true, @@ -50,6 +51,7 @@ export const demoDb: DatabaseModel = { target: OntimeView.Timer, search: 'hideclock=true&hidecards=true&hideprogress=true&hidemessage=true&hidesecondary=true&hidelogo=true&font=arial+black&keycolour=00ff00&timerColour=ffffff', + displayInNav: false, }, ], customFields: { diff --git a/packages/types/src/definitions/core/UrlPreset.type.ts b/packages/types/src/definitions/core/UrlPreset.type.ts index 92d9d9337..57f070225 100644 --- a/packages/types/src/definitions/core/UrlPreset.type.ts +++ b/packages/types/src/definitions/core/UrlPreset.type.ts @@ -20,6 +20,7 @@ type BaseURLPreset = { enabled: boolean; alias: string; search: string; + displayInNav: boolean; options?: Record; }; @@ -28,6 +29,7 @@ type CuesheetUrlPreset = { enabled: boolean; alias: string; search: string; + displayInNav: boolean; options?: { read?: string; write?: string;