mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-19 06:04:05 +00:00
feat: cuesheet sharing
feat: locked presets refactor: simplify locked param refactor: create share links
This commit is contained in:
committed by
Carlos Valente
parent
1695b4dc68
commit
6c6f5c2c0f
@@ -16,4 +16,16 @@
|
||||
.copiableLink {
|
||||
user-select: text;
|
||||
color: $ui-white;
|
||||
}
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.shareInline {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 172px;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.end {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
@@ -1,70 +1,157 @@
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRef, useState } from 'react';
|
||||
import { FieldErrors, useForm } from 'react-hook-form';
|
||||
import QRCode from 'react-qr-code';
|
||||
import { OntimeView, URLPreset } from 'ontime-types';
|
||||
import { generateId } from 'ontime-utils';
|
||||
|
||||
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 Select from '../../common/components/select/Select';
|
||||
import Switch from '../../common/components/switch/Switch';
|
||||
import { useUpdateUrlPreset } from '../../common/hooks-query/useUrlPresets';
|
||||
import copyToClipboard from '../../common/utils/copyToClipboard';
|
||||
import { preventEscape } from '../../common/utils/keyEvent';
|
||||
import { linkToOtherHost } from '../../common/utils/linkUtils';
|
||||
import { currentHostName, isOntimeCloud, serverURL } from '../../externals';
|
||||
import { isUrlSafe } from '../../common/utils/regex';
|
||||
import { isOntimeCloud, serverURL } from '../../externals';
|
||||
import * as Panel from '../app-settings/panel-utils/PanelUtils';
|
||||
|
||||
import CuesheetLinkOptions from './composite/CuesheetLinkOptions';
|
||||
|
||||
import style from './GenerateLinkForm.module.scss';
|
||||
|
||||
interface GenerateLinkFormProps {
|
||||
hostOptions: { value: string; label: string }[];
|
||||
pathOptions: { value: string; label: string }[];
|
||||
pathOptions: { value: OntimeView | string; label: string }[];
|
||||
presets: URLPreset[];
|
||||
isLockedToView?: boolean;
|
||||
}
|
||||
|
||||
interface GenerateLinkFormOptions {
|
||||
type GenericLinkOptions = {
|
||||
baseUrl: string;
|
||||
path: string;
|
||||
lock: boolean;
|
||||
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, isLockedToView }: GenerateLinkFormProps) {
|
||||
export default function GenerateLinkForm({ hostOptions, pathOptions, presets, isLockedToView }: GenerateLinkFormProps) {
|
||||
const [formState, setFormState] = useState<GenerateLinkState>('pending');
|
||||
const [url, setUrl] = useState(serverURL);
|
||||
const cuesheetReadRef = useRef<HTMLInputElement>(null);
|
||||
const cuesheetWriteRef = useRef<HTMLInputElement>(null);
|
||||
const generatedAlias = useRef<string>(`cuesheet-${generateId()}`);
|
||||
|
||||
const { addPreset } = useUpdateUrlPreset();
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
setError,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
reset,
|
||||
register,
|
||||
formState: { errors, isDirty },
|
||||
} = useForm<GenerateLinkFormOptions>({
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
baseUrl: currentHostName,
|
||||
path: isLockedToView ? pathOptions[0].value : 'timer',
|
||||
lock: false,
|
||||
baseUrl: serverURL,
|
||||
path: isLockedToView ? pathOptions[0].value : OntimeView.Timer,
|
||||
authenticate: false,
|
||||
},
|
||||
resetOptions: {
|
||||
keepDirtyValues: true,
|
||||
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<CuesheetLinkOptions['options']>,
|
||||
): Promise<URLPreset | undefined> => {
|
||||
if (options.read === '-') {
|
||||
throw new Error('Cannot create a share with no read permissions');
|
||||
}
|
||||
const presets = await addPreset({
|
||||
target: OntimeView.Cuesheet,
|
||||
enabled: true,
|
||||
alias,
|
||||
search: '',
|
||||
options: {
|
||||
read: options.read,
|
||||
write: options.write,
|
||||
},
|
||||
});
|
||||
return presets.find((preset) => preset.alias === alias);
|
||||
};
|
||||
|
||||
const onSubmit = async (options: GenerateLinkFormOptions) => {
|
||||
try {
|
||||
setFormState('loading');
|
||||
const baseUrl = linkToOtherHost(options.baseUrl);
|
||||
const url = await generateUrl(baseUrl, options.path, options.lock, options.authenticate);
|
||||
await copyToClipboard(url);
|
||||
setUrl(url);
|
||||
if (options.path === OntimeView.Cuesheet) {
|
||||
const urlPreset = await createPresetFromOptions((options as CuesheetLinkOptions).alias, {
|
||||
read: cuesheetReadRef.current?.value ?? 'full',
|
||||
write: cuesheetWriteRef.current?.value ?? 'full',
|
||||
});
|
||||
|
||||
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 copyToClipboard(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: linkToOtherHost(options.baseUrl),
|
||||
path,
|
||||
authenticate: options.authenticate,
|
||||
lockConfig: options.lockConfig,
|
||||
lockNav: options.lockNav,
|
||||
preset: presetPath,
|
||||
});
|
||||
|
||||
await copyToClipboard(url);
|
||||
setUrl(url);
|
||||
}
|
||||
reset(options, {
|
||||
keepValues: true,
|
||||
keepDirty: false,
|
||||
});
|
||||
setFormState('success');
|
||||
setTimeout(() => {
|
||||
setFormState('pending');
|
||||
}, 4000);
|
||||
} catch (error) {
|
||||
const message = maybeAxiosError(error);
|
||||
setError('root', { message });
|
||||
@@ -72,70 +159,113 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, isLockedToV
|
||||
}
|
||||
};
|
||||
|
||||
const canSubmit = isDirty || formState !== 'success';
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} onKeyDown={(event) => preventEscape(event)}>
|
||||
{errors.root && <Panel.Error>{errors.root.message}</Panel.Error>}
|
||||
{!isLockedToView ? (
|
||||
{!isLockedToView && (
|
||||
<Info>You can generate a link to share with your team or to use in automation (such as companion).</Info>
|
||||
) : (
|
||||
<Info>You can generate a link to share with your team</Info>
|
||||
)}
|
||||
<Panel.ListGroup>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field
|
||||
title='Host IP'
|
||||
description={`Which IP address will be used${isOntimeCloud ? ' (not applicable in Ontime Cloud)' : ''}`}
|
||||
/>
|
||||
<Select
|
||||
disabled={isOntimeCloud}
|
||||
options={hostOptions}
|
||||
value={watch('baseUrl')}
|
||||
onValueChange={(value) => setValue('baseUrl', value)}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
{isLockedToView ? (
|
||||
<input type='hidden' value={watch('path')} />
|
||||
) : (
|
||||
<Panel.ListItem>
|
||||
<Panel.Field title='Ontime view' description='Which view or preset will the link point to' />
|
||||
<Select options={pathOptions} value={watch('path')} onValueChange={(value) => setValue('path', value)} />
|
||||
</Panel.ListItem>
|
||||
)}
|
||||
<div className={style.shareInline}>
|
||||
<div className={style.column}>
|
||||
<Panel.ListGroup>
|
||||
{isOntimeCloud ? (
|
||||
<input hidden readOnly name='baseUrl' value={serverURL} />
|
||||
) : (
|
||||
<Panel.ListItem>
|
||||
<Panel.Field
|
||||
title='Host IP'
|
||||
description={`Which IP address will be used${isOntimeCloud ? ' (not applicable in Ontime Cloud)' : ''}`}
|
||||
/>
|
||||
<Select
|
||||
options={hostOptions}
|
||||
value={watch('baseUrl')}
|
||||
onValueChange={(value) => setValue('baseUrl', value)}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
)}
|
||||
{isLockedToView ? (
|
||||
<input type='hidden' value={watch('path')} />
|
||||
) : (
|
||||
<Panel.ListItem>
|
||||
<Panel.Field title='Ontime view' description='Which view or preset will the link point to' />
|
||||
<Select
|
||||
options={pathOptions}
|
||||
value={watch('path')}
|
||||
onValueChange={(value) => setValue('path', value, { shouldDirty: true })}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
)}
|
||||
|
||||
<Panel.ListItem>
|
||||
<Panel.Field
|
||||
title='Lock navigation'
|
||||
description='Prevent showing navigation (will only work for non production URLs)'
|
||||
/>
|
||||
<Switch
|
||||
size='large'
|
||||
name='lock'
|
||||
checked={watch('lock')}
|
||||
onCheckedChange={(checked) => setValue('lock', checked)}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field title='Authenticate' description='Whether the URL should be pre-authenticated' />
|
||||
<Switch
|
||||
size='large'
|
||||
name='authenticate'
|
||||
checked={watch('authenticate')}
|
||||
onCheckedChange={(checked) => setValue('authenticate', checked)}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
</Panel.ListGroup>
|
||||
<Panel.ListGroup>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field title='Generate link' description='Fill form and generate link and QR code' />
|
||||
<Button variant='primary' loading={formState === 'loading'} type='submit' style={{ alignSelf: 'end' }}>
|
||||
{formState === 'success' ? 'Link copied to clipboard!' : 'Update share link'}
|
||||
</Button>
|
||||
<div className={style.column}>
|
||||
<QRCode size={172} value={url} className={style.qrCode} />
|
||||
<div className={style.copiableLink}>{url}</div>
|
||||
{watch('path') === OntimeView.Cuesheet && (
|
||||
<>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field
|
||||
title='Preset alias'
|
||||
description='The name of the preset we will create to hold this options'
|
||||
error={(errors as FieldErrors<CuesheetLinkOptions>).alias?.message}
|
||||
/>
|
||||
<Input
|
||||
defaultValue={generatedAlias.current}
|
||||
{...register('alias', {
|
||||
required: 'Alias cannot be empty and must be unique',
|
||||
pattern: {
|
||||
value: isUrlSafe,
|
||||
message: 'Field can only contain URL safe characters (a-z, 0-9, _ and -)',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
<CuesheetLinkOptions readRef={cuesheetReadRef} writeRef={cuesheetWriteRef} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Panel.ListItem>
|
||||
<Panel.Field title='Lock navigation' description='Whether to hide the navigation menu' />
|
||||
<Switch
|
||||
size='large'
|
||||
name='lockNav'
|
||||
checked={watch('lockNav')}
|
||||
onCheckedChange={(checked) => setValue('lockNav', checked, { shouldDirty: true })}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
{watch('path') !== OntimeView.Cuesheet && (
|
||||
<Panel.ListItem>
|
||||
<Panel.Field title='Lock configuration' description='Whether to hide the configuration panel' />
|
||||
<Switch
|
||||
size='large'
|
||||
name='lockConfig'
|
||||
checked={watch('lockConfig')}
|
||||
onCheckedChange={(checked) => setValue('lockConfig', checked, { shouldDirty: true })}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
)}
|
||||
<Panel.ListItem>
|
||||
<Panel.Field title='Authenticate' description='Whether the URL should be pre-authenticated' />
|
||||
<Switch
|
||||
size='large'
|
||||
name='authenticate'
|
||||
checked={watch('authenticate')}
|
||||
onCheckedChange={(checked) => setValue('authenticate', checked, { shouldDirty: true })}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
</Panel.ListGroup>
|
||||
<Panel.Error>{errors.root?.message}</Panel.Error>
|
||||
<Panel.InlineElements align='end' className={style.end}>
|
||||
<Button type='submit' variant={canSubmit ? 'primary' : 'subtle'} loading={formState === 'loading'}>
|
||||
{canSubmit ? 'Create share link' : 'Link copied to clipboard!'}
|
||||
</Button>
|
||||
</Panel.InlineElements>
|
||||
</div>
|
||||
<Panel.Section className={style.column}>
|
||||
<Panel.Description>Share this link</Panel.Description>
|
||||
<QRCode size={172} value={url} className={style.qrCode} />
|
||||
<div className={style.copiableLink} data-testid='copy-link'>
|
||||
{url}
|
||||
</div>
|
||||
</Panel.ListItem>
|
||||
</Panel.ListGroup>
|
||||
<CopyTag copyValue={url}>Copy link</CopyTag>
|
||||
</Panel.Section>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { OntimeView } from 'ontime-types';
|
||||
|
||||
import useInfo from '../../common/hooks-query/useInfo';
|
||||
import useUrlPresets from '../../common/hooks-query/useUrlPresets';
|
||||
@@ -6,37 +7,42 @@ import useUrlPresets from '../../common/hooks-query/useUrlPresets';
|
||||
import GenerateLinkForm from './GenerateLinkForm';
|
||||
|
||||
interface GenerateLinkFormExportProps {
|
||||
lockedPath?: { value: string; label: string };
|
||||
lockedPath?: { value: OntimeView; label: string };
|
||||
}
|
||||
|
||||
export default function GenerateLinkFormExport({ lockedPath }: GenerateLinkFormExportProps) {
|
||||
const { data: infoData } = useInfo();
|
||||
const { data: urlPresetData } = useUrlPresets({ skip: lockedPath === undefined });
|
||||
|
||||
const hostOptions = useMemo(
|
||||
() =>
|
||||
infoData.networkInterfaces.map((nif) => ({
|
||||
value: nif.address,
|
||||
label: `${nif.name} - ${nif.address}`,
|
||||
})),
|
||||
[infoData.networkInterfaces],
|
||||
);
|
||||
const hostOptions = useMemo(() => {
|
||||
return infoData.networkInterfaces.map((nif) => ({
|
||||
value: nif.address,
|
||||
label: `${nif.name} - ${nif.address}`,
|
||||
}));
|
||||
}, [infoData.networkInterfaces]);
|
||||
|
||||
const pathOptions = useMemo(() => {
|
||||
if (lockedPath) {
|
||||
return [{ value: lockedPath.value, label: lockedPath.label }];
|
||||
}
|
||||
return [
|
||||
{ value: 'timer', label: 'Timer' },
|
||||
{ value: 'cuesheet', label: 'Cuesheet' },
|
||||
{ value: 'op', label: 'Operator' },
|
||||
{ value: OntimeView.Timer, label: 'Timer' },
|
||||
{ value: OntimeView.Cuesheet, label: 'Cuesheet' },
|
||||
{ value: OntimeView.Operator, label: 'Operator' },
|
||||
{ value: '', label: 'Companion' },
|
||||
...urlPresetData.map((preset) => ({
|
||||
value: preset.alias,
|
||||
value: `preset-${preset.alias}`,
|
||||
label: `URL Preset: ${preset.alias}`,
|
||||
})),
|
||||
];
|
||||
}, [lockedPath, urlPresetData]);
|
||||
|
||||
return <GenerateLinkForm hostOptions={hostOptions} pathOptions={pathOptions} isLockedToView={Boolean(lockedPath)} />;
|
||||
return (
|
||||
<GenerateLinkForm
|
||||
hostOptions={hostOptions}
|
||||
pathOptions={pathOptions}
|
||||
presets={urlPresetData}
|
||||
isLockedToView={Boolean(lockedPath)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
.twoCols {
|
||||
display: grid;
|
||||
grid-template-columns: max-content max-content;
|
||||
column-gap: 3rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, max-content);
|
||||
column-gap: 1rem;
|
||||
row-gap: 0.5rem;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import { Fragment, RefObject, useMemo, useState } from 'react';
|
||||
|
||||
import RadioGroup from '../../../common/components/radio-group/RadioGroup';
|
||||
import Switch from '../../../common/components/switch/Switch';
|
||||
import useCustomFields from '../../../common/hooks-query/useCustomFields';
|
||||
import { cuesheetDefaultColumns, makeCuesheetCustomColumns } from '../../../views/cuesheet/cuesheet.options';
|
||||
import * as Panel from '../../app-settings/panel-utils/PanelUtils';
|
||||
|
||||
import style from './CuesheetLinkOptions.module.scss';
|
||||
|
||||
type AccessMode = 'full' | 'custom';
|
||||
|
||||
interface CuesheetLinkOptionsProps {
|
||||
readRef?: RefObject<HTMLInputElement | null>;
|
||||
writeRef?: RefObject<HTMLInputElement | null>;
|
||||
}
|
||||
|
||||
export default function CuesheetLinkOptions({ readRef, writeRef }: CuesheetLinkOptionsProps) {
|
||||
const { data } = useCustomFields();
|
||||
const customFieldColumns = useMemo(() => makeCuesheetCustomColumns(data), [data]);
|
||||
|
||||
const [readPermissions, setReadPermissions] = useState<AccessMode>('full');
|
||||
const [writePermissions, setWritePermissions] = useState<AccessMode>('full');
|
||||
|
||||
const [readSwitches, setReadSwitches] = useState<Record<string, boolean>>(() => {
|
||||
const initialState: Record<string, boolean> = {};
|
||||
[...cuesheetDefaultColumns, ...customFieldColumns].forEach((column) => {
|
||||
initialState[column.value] = true;
|
||||
});
|
||||
return initialState;
|
||||
});
|
||||
|
||||
const [writeSwitches, setWriteSwitches] = useState<Record<string, boolean>>(() => {
|
||||
const initialState: Record<string, boolean> = {};
|
||||
[...cuesheetDefaultColumns, ...customFieldColumns].forEach((column) => {
|
||||
initialState[column.value] = true;
|
||||
});
|
||||
return initialState;
|
||||
});
|
||||
|
||||
const handleReadModeChange = (value: AccessMode) => {
|
||||
setReadPermissions(value);
|
||||
|
||||
setReadSwitches((prevReadSwitches) => {
|
||||
const updatedReadSwitches = { ...prevReadSwitches };
|
||||
Object.keys(updatedReadSwitches).forEach((key) => {
|
||||
updatedReadSwitches[key] = true;
|
||||
});
|
||||
return updatedReadSwitches;
|
||||
});
|
||||
};
|
||||
|
||||
const handleWriteModeChange = (value: AccessMode) => {
|
||||
if (value === '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) => {
|
||||
if (type === 'read') {
|
||||
setReadSwitches((prevReadSwitches) => {
|
||||
const updatedReadSwitches = { ...prevReadSwitches, [key]: value };
|
||||
return updatedReadSwitches;
|
||||
});
|
||||
} else {
|
||||
setWriteSwitches((prevWriteSwitches) => {
|
||||
const updatedWriteSwitches = { ...prevWriteSwitches, [key]: value };
|
||||
return updatedWriteSwitches;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getReadPermissions = () => {
|
||||
if (readPermissions === 'full' || writePermissions === 'full') {
|
||||
return 'full';
|
||||
}
|
||||
|
||||
return Object.entries(readSwitches)
|
||||
.filter(([_, value]) => value)
|
||||
.map(([key]) => key)
|
||||
.join(',');
|
||||
};
|
||||
|
||||
const getWritePermissions = () => {
|
||||
if (writePermissions === 'full') {
|
||||
return 'full';
|
||||
}
|
||||
|
||||
return Object.entries(writeSwitches)
|
||||
.filter(([_, value]) => value)
|
||||
.map(([key]) => key)
|
||||
.join(',');
|
||||
};
|
||||
|
||||
return (
|
||||
<Panel.Indent>
|
||||
<input name='read' hidden readOnly ref={readRef} value={getReadPermissions() || '-'} />
|
||||
<input name='write' hidden readOnly ref={writeRef} value={getWritePermissions() || '-'} />
|
||||
<div>
|
||||
<Panel.Field title='Access mode' description='Which parts of the data will the link give access to' />
|
||||
<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
|
||||
value={readPermissions}
|
||||
onValueChange={handleReadModeChange}
|
||||
orientation='horizontal'
|
||||
disabled={writePermissions === 'full'}
|
||||
items={[
|
||||
{ value: 'full', label: 'Full read (view all existing and future columns)' },
|
||||
{ value: 'custom', label: 'Custom read' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.twoCols}>
|
||||
<div className={style.grid}>
|
||||
<Panel.Description>Ontime columns</Panel.Description>
|
||||
<Panel.Description>Read</Panel.Description>
|
||||
<Panel.Description>Write</Panel.Description>
|
||||
{cuesheetDefaultColumns.map((column) => (
|
||||
<Fragment key={column.value}>
|
||||
<div>{column.label}</div>
|
||||
<Switch
|
||||
checked={Boolean(readSwitches[column.value])}
|
||||
onCheckedChange={(value: boolean) => handleSwitchChange(column.value, 'read', value)}
|
||||
disabled={readPermissions === 'full' || writePermissions === 'full'}
|
||||
data-testid={`read-${column.value}`}
|
||||
/>
|
||||
<Switch
|
||||
checked={Boolean(writeSwitches[column.value])}
|
||||
onCheckedChange={(value: boolean) => handleSwitchChange(column.value, 'write', value)}
|
||||
disabled={writePermissions === 'full'}
|
||||
data-testid={`write-${column.value}`}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
{customFieldColumns.length > 0 && (
|
||||
<div className={style.grid}>
|
||||
<Panel.Description>Custom fields</Panel.Description>
|
||||
<Panel.Description>Read</Panel.Description>
|
||||
<Panel.Description>Write</Panel.Description>
|
||||
{customFieldColumns.map((column) => (
|
||||
<Fragment key={column.value}>
|
||||
{column.label}
|
||||
<Switch
|
||||
checked={Boolean(readSwitches[column.value])}
|
||||
onCheckedChange={(value: boolean) => handleSwitchChange(column.value, 'read', value)}
|
||||
disabled={readPermissions === 'full' || writePermissions === 'full'}
|
||||
data-testid={`read-${column.value}`}
|
||||
/>
|
||||
<Switch
|
||||
checked={Boolean(writeSwitches[column.value])}
|
||||
onCheckedChange={(value: boolean) => handleSwitchChange(column.value, 'write', value)}
|
||||
disabled={writePermissions === 'full'}
|
||||
data-testid={`write-${column.value}`}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Panel.Indent>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user