import { OntimeView, URLPreset } from 'ontime-types'; import { generateId } from 'ontime-utils'; import { useCallback, useRef, useState } from 'react'; import { FieldErrors, useForm } from 'react-hook-form'; import { generateUrl } from '../../common/api/session'; import { maybeAxiosError } from '../../common/api/utils'; import Button from '../../common/components/buttons/Button'; import CopyTag from '../../common/components/copy-tag/CopyTag'; import Info from '../../common/components/info/Info'; import Input from '../../common/components/input/input/Input'; import QRCode from '../../common/components/qr-code/QrCode'; import Select from '../../common/components/select/Select'; import Switch from '../../common/components/switch/Switch'; import { useUpdateUrlPreset } from '../../common/hooks-query/useUrlPresets'; import { safeCopyToClipboard } from '../../common/utils/copyToClipboard'; import { preventEscape } from '../../common/utils/keyEvent'; import { isUrlSafe } from '../../common/utils/regex'; import { isOntimeCloud, serverURL } from '../../externals'; import * as Panel from '../app-settings/panel-utils/PanelUtils'; import CuesheetLinkOptions, { CuesheetPermissionValues } from './composite/CuesheetLinkOptions'; import style from './GenerateLinkForm.module.scss'; interface GenerateLinkFormProps { hostOptions: { value: string; label: string }[]; pathOptions: { value: OntimeView | string; label: string }[]; presets: URLPreset[]; isLockedToView?: boolean; } type GenericLinkOptions = { baseUrl: string; path: OntimeView | string; // we use empty string for Companion view authenticate: boolean; lockConfig: boolean; lockNav: boolean; }; type CuesheetLinkOptions = GenericLinkOptions & { path: OntimeView.Cuesheet; alias: string; options: { read?: string; write?: string; }; }; type GenerateLinkFormOptions = GenericLinkOptions | CuesheetLinkOptions; type GenerateLinkState = 'pending' | 'loading' | 'success' | 'error'; export default function GenerateLinkForm({ hostOptions, pathOptions, presets, isLockedToView }: GenerateLinkFormProps) { const [formState, setFormState] = useState('pending'); const [url, setUrl] = useState(''); const [cuesheetPermissions, setCuesheetPermissions] = useState({ read: 'full', write: 'full', }); const generatedAlias = useRef(`cuesheet-${generateId()}`); const { addPreset, updatePreset } = useUpdateUrlPreset(); // Tracks the alias we already created this session so re-generating updates rather than duplicates it const createdAlias = useRef(null); /** * Permissions live outside react-hook-form, so we reset a successful state manually * whenever they change - this re-arms the "Create share link" button as the previous * link no longer reflects the selected permissions. */ const handlePermissionsChange = useCallback((permissions: CuesheetPermissionValues) => { setCuesheetPermissions(permissions); setFormState((current) => (current === 'success' ? 'pending' : current)); }, []); const { handleSubmit, setError, watch, setValue, reset, register, formState: { errors, isDirty }, } = useForm({ mode: 'onChange', defaultValues: { baseUrl: serverURL, path: isLockedToView ? pathOptions[0].value : OntimeView.Timer, authenticate: false, lockConfig: false, lockNav: false, }, }); /** * If the user is generating a link to the cuesheet we gather extra options * The extra options are saved into a URL preset which we then request a share link for */ const createPresetFromOptions = async ( alias: string, options: Required, ): Promise => { if (options.read === '-') { throw new Error('Cannot create a share with no read permissions'); } const payload = { target: OntimeView.Cuesheet, enabled: true, alias, search: '', displayInNav: false, options: { read: options.read, write: options.write, }, } as const; // Re-generating with the same name updates the existing preset instead of failing on a duplicate alias const presets = createdAlias.current === alias ? await updatePreset(alias, payload) : await addPreset(payload); createdAlias.current = alias; return presets.find((preset) => preset.alias === alias); }; const onSubmit = async (options: GenerateLinkFormOptions) => { try { setFormState('loading'); if (options.path === OntimeView.Cuesheet) { const urlPreset = await createPresetFromOptions((options as CuesheetLinkOptions).alias, { read: cuesheetPermissions.read, write: cuesheetPermissions.write, }); if (!urlPreset) { throw new Error('Failed to create URL preset for Cuesheet'); } const url = await generateUrl({ baseUrl: options.baseUrl, path: options.path, authenticate: options.authenticate, lockConfig: options.lockConfig, lockNav: options.lockNav, preset: urlPreset.alias, }); await safeCopyToClipboard(url); setUrl(url); } else { const presetPath = options.path.startsWith('preset-') ? options.path.replace('preset-', '') : undefined; const path = presetPath ? presets.find((preset) => preset.alias === presetPath)?.target : options.path; if (!path) { throw new Error(`Could not resolve preset: ${path}`); } const url = await generateUrl({ baseUrl: options.baseUrl, path, authenticate: options.authenticate, lockConfig: options.lockConfig, lockNav: options.lockNav, preset: presetPath, }); await safeCopyToClipboard(url); setUrl(url); } reset(options, { keepValues: true, keepDirty: false, }); setFormState('success'); } catch (error) { const message = maybeAxiosError(error); setError('root', { message }); setFormState('error'); } }; const noReadAccess = watch('path') === OntimeView.Cuesheet && cuesheetPermissions.read === '-'; const canSubmit = isDirty || formState !== 'success'; return (
preventEscape(event)}> {!isLockedToView && ( You can generate a link to share with your team or to use in automation (such as companion). )}
{isOntimeCloud ? ( ) : ( ) : ( )} setValue('lockNav', checked, { shouldDirty: true })} disabled={watch('lockConfig')} /> {watch('path') !== OntimeView.Cuesheet && ( { if (checked) { setValue('lockNav', checked, { shouldDirty: true }); } setValue('lockConfig', checked, { shouldDirty: true }); }} /> )} setValue('authenticate', checked, { shouldDirty: true })} /> {errors.root?.message}
Share this link {url ? ( <>
{url}
Copy link ) : ( Your link will appear here once you create it. )}
); }