allow editing cuesheet link permissions

This commit is contained in:
Carlos Valente
2026-07-01 13:24:47 +02:00
committed by GitHub
parent 7850358039
commit 42147d4d5b
4 changed files with 258 additions and 140 deletions
@@ -1,4 +1,4 @@
import { URLPreset } from 'ontime-types'; import { OntimeView, URLPreset } from 'ontime-types';
import { useState } from 'react'; import { useState } from 'react';
import { IoAdd, IoOpenOutline, IoPencil, IoTrash } from 'react-icons/io5'; import { IoAdd, IoOpenOutline, IoPencil, IoTrash } from 'react-icons/io5';
@@ -78,20 +78,22 @@ export default function URLPresets() {
<tbody> <tbody>
{data.length === 0 && <Panel.TableEmpty handleClick={openNewForm} />} {data.length === 0 && <Panel.TableEmpty handleClick={openNewForm} />}
{data.map((preset, index) => { {data.map((preset, index) => {
const isCuesheet = preset.target === OntimeView.Cuesheet;
return ( return (
<tr key={preset.alias}> <tr key={preset.alias}>
<td> <td>
<Switch <Switch
checked={preset.enabled} checked={preset.enabled}
onCheckedChange={(checked) => persistPreset({ ...preset, enabled: checked })} onCheckedChange={(enabled) => persistPreset({ ...preset, enabled })}
disabled={isMutating} disabled={isMutating}
aria-label='Toggle preset enabled'
/> />
</td> </td>
<td> <td>
<Switch <Switch
checked={preset.displayInNav} checked={preset.displayInNav}
onCheckedChange={(checked) => persistPreset({ ...preset, displayInNav: checked })} onCheckedChange={(checked) => persistPreset({ ...preset, displayInNav: checked })}
disabled={isMutating} disabled={isMutating || isCuesheet}
/> />
</td> </td>
<td> <td>
@@ -1,5 +1,5 @@
import { OntimeView, OntimeViewPresettable, URLPreset } from 'ontime-types'; import { OntimeView, OntimeViewPresettable, URLPreset } from 'ontime-types';
import { useEffect, useRef } from 'react'; import { useEffect, useRef, useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { maybeAxiosError, unwrapError } from '../../../../../common/api/utils'; import { maybeAxiosError, unwrapError } from '../../../../../common/api/utils';
@@ -12,6 +12,7 @@ import { preventEscape } from '../../../../../common/utils/keyEvent';
import { isUrlSafe } from '../../../../../common/utils/regex'; import { isUrlSafe } from '../../../../../common/utils/regex';
import { enDash } from '../../../../../common/utils/styleUtils'; import { enDash } from '../../../../../common/utils/styleUtils';
import { generateUrlPresetOptions } from '../../../../../common/utils/urlPresets'; import { generateUrlPresetOptions } from '../../../../../common/utils/urlPresets';
import CuesheetLinkOptions, { CuesheetPermissionValues } from '../../../../sharing/composite/CuesheetLinkOptions';
import * as Panel from '../../../panel-utils/PanelUtils'; import * as Panel from '../../../panel-utils/PanelUtils';
import style from './URLPresetForm.module.scss'; import style from './URLPresetForm.module.scss';
@@ -62,12 +63,42 @@ export default function URLPresetForm({ urlPreset, onClose }: URLPresetFormProps
}); });
const urlRef = useRef<HTMLInputElement>(null); const urlRef = useRef<HTMLInputElement>(null);
// Cuesheet read/write permissions live outside react-hook-form
const initialPermissions = useRef<CuesheetPermissionValues>({
read: urlPreset?.options?.read ?? 'full',
write: urlPreset?.options?.write ?? 'full',
});
const [cuesheetPermissions, setCuesheetPermissions] = useState<CuesheetPermissionValues>(initialPermissions.current);
// update initial permissions on mount
useEffect(() => {
initialPermissions.current = {
read: urlPreset?.options?.read ?? 'full',
write: urlPreset?.options?.write ?? 'full',
};
setCuesheetPermissions(initialPermissions.current);
// oxlint-disable-next-line eslint-plugin-react-hooks/exhaustive-deps -- run on mount
}, []);
const isEditingCuesheet = urlPreset && urlPreset.target === OntimeView.Cuesheet;
const isCuesheet = watch('target') === OntimeView.Cuesheet;
const permissionsDirty =
isCuesheet &&
(cuesheetPermissions.read !== initialPermissions.current.read ||
cuesheetPermissions.write !== initialPermissions.current.write);
const noReadAccess = isCuesheet && cuesheetPermissions.read === '-';
const setupSubmit = async (data: URLPreset) => { const setupSubmit = async (data: URLPreset) => {
try { try {
// Preserve / apply cuesheet permissions, which are not part of the form fields
const payload: URLPreset =
data.target === OntimeView.Cuesheet
? { ...data, target: OntimeView.Cuesheet, options: cuesheetPermissions }
: data;
if (urlPreset) { if (urlPreset) {
await updatePreset(urlPreset.alias, data); await updatePreset(urlPreset.alias, payload);
} else { } else {
await addPreset(data); await addPreset(payload);
} }
onClose(); onClose();
} catch (error) { } catch (error) {
@@ -75,6 +106,7 @@ export default function URLPresetForm({ urlPreset, onClose }: URLPresetFormProps
} }
}; };
// focus on alias when the form opens
useEffect(() => { useEffect(() => {
setFocus('alias'); setFocus('alias');
}, [setFocus]); }, [setFocus]);
@@ -127,44 +159,66 @@ export default function URLPresetForm({ urlPreset, onClose }: URLPresetFormProps
<div className={style.expand}> <div className={style.expand}>
<Panel.Description>Generate options (paste URL to generate options)</Panel.Description> <Panel.Description>Generate options (paste URL to generate options)</Panel.Description>
<Panel.InlineElements> <Panel.InlineElements>
<Input placeholder='Paste URL' fluid ref={urlRef} /> <Input placeholder='Paste URL' fluid ref={urlRef} disabled={isEditingCuesheet} />
<Button onClick={generateOptions}>Generate</Button> <Button onClick={generateOptions} disabled={isEditingCuesheet}>
Generate
</Button>
</Panel.InlineElements> </Panel.InlineElements>
</div> </div>
</Panel.InlineElements> </Panel.InlineElements>
{errors.alias?.message && <Panel.Error>{errors.alias.message}</Panel.Error>} {errors.alias?.message && <Panel.Error>{errors.alias.message}</Panel.Error>}
<div> {!isEditingCuesheet && (
{enDash} or {enDash} <>
</div> <div>
<div>2. Choose a view and its parameters</div> {enDash} or {enDash}
<div> </div>
<Panel.Description>Target</Panel.Description> <div>2. Choose a view and its parameters</div>
<Select <div>
options={targetOptions} <Panel.Description>Target</Panel.Description>
{...register('target', { required: 'Target is required' })} <Select
value={watch('target')} options={targetOptions}
onValueChange={(value: OntimeViewPresettable | null) => { {...register('target', { required: 'Target is required' })}
if (value === null) return; value={watch('target')}
setValue('target', value, { shouldDirty: true }); onValueChange={(value: OntimeViewPresettable | null) => {
}} if (value === null) return;
/> setValue('target', value, { shouldDirty: true });
</div> }}
<div> />
<Panel.Description>Parameters</Panel.Description> </div>
<Textarea <div>
fluid <Panel.Description>Parameters</Panel.Description>
rows={3} <Textarea
{...register('search', { fluid
validate: validateParams, rows={3}
})} {...register('search', {
/> validate: validateParams,
<Panel.Error>{errors.search?.message}</Panel.Error> })}
</div> />
<Panel.Error>{errors.search?.message}</Panel.Error>
</div>
</>
)}
{isCuesheet && (
<div>
<Panel.Description>Permissions</Panel.Description>
<CuesheetLinkOptions
initialRead={initialPermissions.current.read}
initialWrite={initialPermissions.current.write}
onChange={setCuesheetPermissions}
/>
</div>
)}
<div> <div>
<Panel.Error>{errors.root?.message}</Panel.Error> <Panel.Error>{errors.root?.message}</Panel.Error>
<Panel.InlineElements align='end'> <Panel.InlineElements align='end'>
<Button onClick={onClose}>Cancel</Button> <Button onClick={onClose}>Cancel</Button>
<Button variant='primary' type='submit' disabled={!isValid || !isDirty} loading={isSubmitting || isMutating}> <Button
variant='primary'
type='submit'
disabled={!isValid || (!isDirty && !permissionsDirty) || noReadAccess}
loading={isSubmitting || isMutating}
>
Save Save
</Button> </Button>
</Panel.InlineElements> </Panel.InlineElements>
@@ -1,6 +1,6 @@
import { OntimeView, URLPreset } from 'ontime-types'; import { OntimeView, URLPreset } from 'ontime-types';
import { generateId } from 'ontime-utils'; import { generateId } from 'ontime-utils';
import { useRef, useState } from 'react'; import { useCallback, useRef, useState } from 'react';
import { FieldErrors, useForm } from 'react-hook-form'; import { FieldErrors, useForm } from 'react-hook-form';
import { generateUrl } from '../../common/api/session'; import { generateUrl } from '../../common/api/session';
@@ -18,7 +18,7 @@ import { preventEscape } from '../../common/utils/keyEvent';
import { isUrlSafe } from '../../common/utils/regex'; import { isUrlSafe } from '../../common/utils/regex';
import { isOntimeCloud, serverURL } from '../../externals'; import { isOntimeCloud, serverURL } from '../../externals';
import * as Panel from '../app-settings/panel-utils/PanelUtils'; import * as Panel from '../app-settings/panel-utils/PanelUtils';
import CuesheetLinkOptions from './composite/CuesheetLinkOptions'; import CuesheetLinkOptions, { CuesheetPermissionValues } from './composite/CuesheetLinkOptions';
import style from './GenerateLinkForm.module.scss'; import style from './GenerateLinkForm.module.scss';
@@ -53,12 +53,26 @@ type GenerateLinkState = 'pending' | 'loading' | 'success' | 'error';
export default function GenerateLinkForm({ hostOptions, pathOptions, presets, isLockedToView }: GenerateLinkFormProps) { export default function GenerateLinkForm({ hostOptions, pathOptions, presets, isLockedToView }: GenerateLinkFormProps) {
const [formState, setFormState] = useState<GenerateLinkState>('pending'); const [formState, setFormState] = useState<GenerateLinkState>('pending');
const [url, setUrl] = useState(serverURL); const [url, setUrl] = useState('');
const cuesheetReadRef = useRef<HTMLInputElement>(null); const [cuesheetPermissions, setCuesheetPermissions] = useState<CuesheetPermissionValues>({
const cuesheetWriteRef = useRef<HTMLInputElement>(null); read: 'full',
write: 'full',
});
const generatedAlias = useRef<string>(`cuesheet-${generateId()}`); const generatedAlias = useRef<string>(`cuesheet-${generateId()}`);
const { addPreset } = useUpdateUrlPreset(); const { addPreset, updatePreset } = useUpdateUrlPreset();
// Tracks the alias we already created this session so re-generating updates rather than duplicates it
const createdAlias = useRef<string | null>(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 { const {
handleSubmit, handleSubmit,
@@ -90,7 +104,7 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, presets, is
if (options.read === '-') { if (options.read === '-') {
throw new Error('Cannot create a share with no read permissions'); throw new Error('Cannot create a share with no read permissions');
} }
const presets = await addPreset({ const payload = {
target: OntimeView.Cuesheet, target: OntimeView.Cuesheet,
enabled: true, enabled: true,
alias, alias,
@@ -100,7 +114,10 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, presets, is
read: options.read, read: options.read,
write: options.write, 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); return presets.find((preset) => preset.alias === alias);
}; };
@@ -109,8 +126,8 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, presets, is
setFormState('loading'); setFormState('loading');
if (options.path === OntimeView.Cuesheet) { if (options.path === OntimeView.Cuesheet) {
const urlPreset = await createPresetFromOptions((options as CuesheetLinkOptions).alias, { const urlPreset = await createPresetFromOptions((options as CuesheetLinkOptions).alias, {
read: cuesheetReadRef.current?.value ?? 'full', read: cuesheetPermissions.read,
write: cuesheetWriteRef.current?.value ?? 'full', write: cuesheetPermissions.write,
}); });
if (!urlPreset) { if (!urlPreset) {
@@ -158,6 +175,7 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, presets, is
} }
}; };
const noReadAccess = watch('path') === OntimeView.Cuesheet && cuesheetPermissions.read === '-';
const canSubmit = isDirty || formState !== 'success'; const canSubmit = isDirty || formState !== 'success';
return ( return (
@@ -221,7 +239,7 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, presets, is
})} })}
/> />
</Panel.ListItem> </Panel.ListItem>
<CuesheetLinkOptions readRef={cuesheetReadRef} writeRef={cuesheetWriteRef} /> <CuesheetLinkOptions onChange={handlePermissionsChange} />
</> </>
)} )}
@@ -269,18 +287,29 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, presets, is
</Panel.ListGroup> </Panel.ListGroup>
<Panel.Error>{errors.root?.message}</Panel.Error> <Panel.Error>{errors.root?.message}</Panel.Error>
<Panel.InlineElements align='end' className={style.end}> <Panel.InlineElements align='end' className={style.end}>
<Button type='submit' variant={canSubmit ? 'primary' : 'subtle'} loading={formState === 'loading'}> <Button
type='submit'
variant={canSubmit ? 'primary' : 'subtle'}
loading={formState === 'loading'}
disabled={noReadAccess}
>
{canSubmit ? 'Create share link' : 'Link copied to clipboard!'} {canSubmit ? 'Create share link' : 'Link copied to clipboard!'}
</Button> </Button>
</Panel.InlineElements> </Panel.InlineElements>
</div> </div>
<Panel.Section className={style.column}> <Panel.Section className={style.column}>
<Panel.Description>Share this link</Panel.Description> <Panel.Description>Share this link</Panel.Description>
<QRCode size={172} value={url} /> {url ? (
<div className={style.copiableLink} data-testid='copy-link'> <>
{url} <QRCode size={172} value={url} />
</div> <div className={style.copiableLink} data-testid='copy-link'>
<CopyTag copyValue={url}>Copy link</CopyTag> {url}
</div>
<CopyTag copyValue={url}>Copy link</CopyTag>
</>
) : (
<Panel.Description>Your link will appear here once you create it.</Panel.Description>
)}
</Panel.Section> </Panel.Section>
</div> </div>
</form> </form>
@@ -1,4 +1,4 @@
import { Fragment, RefObject, useMemo, useState } from 'react'; import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
import RadioGroup from '../../../common/components/radio-group/RadioGroup'; import RadioGroup from '../../../common/components/radio-group/RadioGroup';
import Switch from '../../../common/components/switch/Switch'; import Switch from '../../../common/components/switch/Switch';
@@ -10,118 +10,141 @@ import style from './CuesheetLinkOptions.module.scss';
type AccessMode = 'full' | 'custom'; type AccessMode = 'full' | 'custom';
interface CuesheetLinkOptionsProps { export interface CuesheetPermissionValues {
readRef?: RefObject<HTMLInputElement | null>; read: string;
writeRef?: RefObject<HTMLInputElement | null>; write: string;
} }
export default function CuesheetLinkOptions({ readRef, writeRef }: CuesheetLinkOptionsProps) { interface CuesheetLinkOptionsProps {
/** Existing read permission to seed the form with ('full' | '-' | comma separated keys) */
initialRead?: string;
/** Existing write permission to seed the form with ('full' | '-' | comma separated keys) */
initialWrite?: string;
/** Notifies the parent whenever the resolved read/write permissions change */
onChange: (permissions: CuesheetPermissionValues) => void;
}
/** A null result means "full or unset" - there is no explicit per-column selection to seed */
function parseKeys(permission: string | undefined): Set<string> | null {
if (permission == null || permission === 'full') {
return null;
}
if (permission === '-') {
return new Set();
}
return new Set(permission.split(','));
}
function modeFromPermission(permission: string | undefined): AccessMode {
return permission == null || permission === 'full' ? 'full' : 'custom';
}
export default function CuesheetLinkOptions({ initialRead, initialWrite, onChange }: CuesheetLinkOptionsProps) {
const { data } = useCustomFields(); const { data } = useCustomFields();
const customFieldColumns = useMemo(() => makeCuesheetCustomColumns(data), [data]); const customFieldColumns = useMemo(() => makeCuesheetCustomColumns(data), [data]);
const allColumns = useMemo(() => [...cuesheetDefaultColumns, ...customFieldColumns], [customFieldColumns]);
const [readPermissions, setReadPermissions] = useState<AccessMode>('full'); // Parsed seed values - stable for the lifetime of a given preset
const [writePermissions, setWritePermissions] = useState<AccessMode>('full'); const initialReadKeys = useMemo(() => parseKeys(initialRead), [initialRead]);
const initialWriteKeys = useMemo(() => parseKeys(initialWrite), [initialWrite]);
const [readSwitches, setReadSwitches] = useState<Record<string, boolean>>(() => { const [readPermissions, setReadPermissions] = useState<AccessMode>(() => modeFromPermission(initialRead));
const initialState: Record<string, boolean> = {}; const [writePermissions, setWritePermissions] = useState<AccessMode>(() => modeFromPermission(initialWrite));
[...cuesheetDefaultColumns, ...customFieldColumns].forEach((column) => {
initialState[column.value] = true; // Default for a column we have not seen yet: honour the seed in custom mode, otherwise grant access
const defaultRead = useCallback(
(key: string) => (initialReadKeys ? initialReadKeys.has(key) : true),
[initialReadKeys],
);
const defaultWrite = useCallback(
(key: string) => (initialWriteKeys ? initialWriteKeys.has(key) : true),
[initialWriteKeys],
);
const [readSwitches, setReadSwitches] = useState<Record<string, boolean>>({});
const [writeSwitches, setWriteSwitches] = useState<Record<string, boolean>>({});
// Custom fields load asynchronously, so reconcile the switch maps whenever the column list grows.
// Newly seen columns are seeded from the initial values (or default to on for a fresh link).
useEffect(() => {
setReadSwitches((prev) => {
const next = { ...prev };
for (const column of allColumns) {
if (!(column.value in next)) next[column.value] = defaultRead(column.value);
}
return next;
}); });
return initialState; setWriteSwitches((prev) => {
}); const next = { ...prev };
for (const column of allColumns) {
const [writeSwitches, setWriteSwitches] = useState<Record<string, boolean>>(() => { if (!(column.value in next)) next[column.value] = defaultWrite(column.value);
const initialState: Record<string, boolean> = {}; }
[...cuesheetDefaultColumns, ...customFieldColumns].forEach((column) => { return next;
initialState[column.value] = true;
}); });
return initialState; }, [allColumns, defaultRead, defaultWrite]);
});
const isReadOn = (key: string) => readSwitches[key] ?? defaultRead(key);
const isWriteOn = (key: string) => writeSwitches[key] ?? defaultWrite(key);
const handleReadModeChange = (value: AccessMode) => { const handleReadModeChange = (value: AccessMode) => {
setReadPermissions(value); setReadPermissions(value);
setReadSwitches((prevReadSwitches) => {
const updatedReadSwitches = { ...prevReadSwitches };
Object.keys(updatedReadSwitches).forEach((key) => {
updatedReadSwitches[key] = true;
});
return updatedReadSwitches;
});
}; };
const handleWriteModeChange = (value: AccessMode) => { const handleWriteModeChange = (value: AccessMode) => {
setWritePermissions(value);
// Full write implies full read
if (value === 'full') { if (value === 'full') {
setReadPermissions('full'); setReadPermissions('full');
} }
setWritePermissions(value);
setReadSwitches((prevReadSwitches) => {
const updatedReadSwitches = { ...prevReadSwitches };
setWriteSwitches((prevWriteSwitches) => {
const updatedWriteSwitches = { ...prevWriteSwitches };
[...cuesheetDefaultColumns, ...customFieldColumns].forEach((column) => {
updatedReadSwitches[column.value] = true;
updatedWriteSwitches[column.value] = true;
});
return updatedWriteSwitches;
});
return updatedReadSwitches;
});
}; };
const handleSwitchChange = (key: string, type: 'read' | 'write', value: boolean) => { const handleReadSwitch = (key: string, value: boolean) => {
if (type === 'read') { setReadSwitches((prev) => ({ ...prev, [key]: value }));
setReadSwitches((prevReadSwitches) => { // A column the recipient cannot read cannot be written either
const updatedReadSwitches = { ...prevReadSwitches, [key]: value }; if (!value) {
return updatedReadSwitches; setWriteSwitches((prev) => ({ ...prev, [key]: false }));
});
} else {
setWriteSwitches((prevWriteSwitches) => {
const updatedWriteSwitches = { ...prevWriteSwitches, [key]: value };
return updatedWriteSwitches;
});
} }
}; };
const getReadPermissions = () => { const handleWriteSwitch = (key: string, value: boolean) => {
setWriteSwitches((prev) => ({ ...prev, [key]: value }));
// Granting write access requires read access
if (value) {
setReadSwitches((prev) => ({ ...prev, [key]: true }));
}
};
const resolvedRead = useMemo(() => {
if (readPermissions === 'full' || writePermissions === 'full') { if (readPermissions === 'full' || writePermissions === 'full') {
return 'full'; return 'full';
} }
const keys = allColumns.filter((column) => isReadOn(column.value)).map((column) => column.value);
return keys.length ? keys.join(',') : '-';
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [readPermissions, writePermissions, readSwitches, allColumns]);
return Object.entries(readSwitches) const resolvedWrite = useMemo(() => {
.filter(([_, value]) => value)
.map(([key]) => key)
.join(',');
};
const getWritePermissions = () => {
if (writePermissions === 'full') { if (writePermissions === 'full') {
return 'full'; return 'full';
} }
const keys = allColumns.filter((column) => isWriteOn(column.value)).map((column) => column.value);
return keys.length ? keys.join(',') : '-';
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [writePermissions, writeSwitches, allColumns]);
return Object.entries(writeSwitches) // Notify the parent of the resolved permissions. onChange is expected to be stable.
.filter(([_, value]) => value) useEffect(() => {
.map(([key]) => key) onChange({ read: resolvedRead, write: resolvedWrite });
.join(','); // eslint-disable-next-line react-hooks/exhaustive-deps
}; }, [resolvedRead, resolvedWrite]);
const noReadAccess = resolvedRead === '-';
return ( return (
<Panel.Indent> <Panel.Indent>
<input name='read' hidden readOnly ref={readRef} value={getReadPermissions() || '-'} />
<input name='write' hidden readOnly ref={writeRef} value={getWritePermissions() || '-'} />
<div> <div>
<Panel.Field title='Access mode' description='Which parts of the data will the link give access to' /> <Panel.Field title='Access mode' description='Which parts of the data the link gives access to' />
<div> <div>
<RadioGroup
value={writePermissions}
onValueChange={handleWriteModeChange}
orientation='horizontal'
items={[
{ value: 'full', label: 'Full write (edit all existing and future columns)' },
{ value: 'custom', label: 'Custom write' },
]}
/>
<RadioGroup <RadioGroup
value={readPermissions} value={readPermissions}
onValueChange={handleReadModeChange} onValueChange={handleReadModeChange}
@@ -132,8 +155,18 @@ export default function CuesheetLinkOptions({ readRef, writeRef }: CuesheetLinkO
{ value: 'custom', label: 'Custom read' }, { value: 'custom', label: 'Custom read' },
]} ]}
/> />
<RadioGroup
value={writePermissions}
onValueChange={handleWriteModeChange}
orientation='horizontal'
items={[
{ value: 'full', label: 'Full write (edit all existing and future columns)' },
{ value: 'custom', label: 'Custom write' },
]}
/>
</div> </div>
</div> </div>
{noReadAccess && <Panel.Error>Links must contain at least one readable column.</Panel.Error>}
<div className={style.twoCols}> <div className={style.twoCols}>
<div className={style.grid}> <div className={style.grid}>
<Panel.Description>Ontime columns</Panel.Description> <Panel.Description>Ontime columns</Panel.Description>
@@ -143,14 +176,14 @@ export default function CuesheetLinkOptions({ readRef, writeRef }: CuesheetLinkO
<Fragment key={column.value}> <Fragment key={column.value}>
<div>{column.label}</div> <div>{column.label}</div>
<Switch <Switch
checked={Boolean(readSwitches[column.value])} checked={isReadOn(column.value)}
onCheckedChange={(value: boolean) => handleSwitchChange(column.value, 'read', value)} onCheckedChange={(value: boolean) => handleReadSwitch(column.value, value)}
disabled={readPermissions === 'full' || writePermissions === 'full'} disabled={readPermissions === 'full' || writePermissions === 'full'}
data-testid={`read-${column.value}`} data-testid={`read-${column.value}`}
/> />
<Switch <Switch
checked={Boolean(writeSwitches[column.value])} checked={isWriteOn(column.value)}
onCheckedChange={(value: boolean) => handleSwitchChange(column.value, 'write', value)} onCheckedChange={(value: boolean) => handleWriteSwitch(column.value, value)}
disabled={writePermissions === 'full'} disabled={writePermissions === 'full'}
data-testid={`write-${column.value}`} data-testid={`write-${column.value}`}
/> />
@@ -164,16 +197,16 @@ export default function CuesheetLinkOptions({ readRef, writeRef }: CuesheetLinkO
<Panel.Description>Write</Panel.Description> <Panel.Description>Write</Panel.Description>
{customFieldColumns.map((column) => ( {customFieldColumns.map((column) => (
<Fragment key={column.value}> <Fragment key={column.value}>
{column.label} <div>{column.label}</div>
<Switch <Switch
checked={Boolean(readSwitches[column.value])} checked={isReadOn(column.value)}
onCheckedChange={(value: boolean) => handleSwitchChange(column.value, 'read', value)} onCheckedChange={(value: boolean) => handleReadSwitch(column.value, value)}
disabled={readPermissions === 'full' || writePermissions === 'full'} disabled={readPermissions === 'full' || writePermissions === 'full'}
data-testid={`read-${column.value}`} data-testid={`read-${column.value}`}
/> />
<Switch <Switch
checked={Boolean(writeSwitches[column.value])} checked={isWriteOn(column.value)}
onCheckedChange={(value: boolean) => handleSwitchChange(column.value, 'write', value)} onCheckedChange={(value: boolean) => handleWriteSwitch(column.value, value)}
disabled={writePermissions === 'full'} disabled={writePermissions === 'full'}
data-testid={`write-${column.value}`} data-testid={`write-${column.value}`}
/> />