mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-11 10:23:54 +00:00
allow editing cuesheet link permissions
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { URLPreset } from 'ontime-types';
|
||||
import { OntimeView, URLPreset } from 'ontime-types';
|
||||
import { useState } from 'react';
|
||||
import { IoAdd, IoOpenOutline, IoPencil, IoTrash } from 'react-icons/io5';
|
||||
|
||||
@@ -78,20 +78,22 @@ export default function URLPresets() {
|
||||
<tbody>
|
||||
{data.length === 0 && <Panel.TableEmpty handleClick={openNewForm} />}
|
||||
{data.map((preset, index) => {
|
||||
const isCuesheet = preset.target === OntimeView.Cuesheet;
|
||||
return (
|
||||
<tr key={preset.alias}>
|
||||
<td>
|
||||
<Switch
|
||||
checked={preset.enabled}
|
||||
onCheckedChange={(checked) => persistPreset({ ...preset, enabled: checked })}
|
||||
onCheckedChange={(enabled) => persistPreset({ ...preset, enabled })}
|
||||
disabled={isMutating}
|
||||
aria-label='Toggle preset enabled'
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Switch
|
||||
checked={preset.displayInNav}
|
||||
onCheckedChange={(checked) => persistPreset({ ...preset, displayInNav: checked })}
|
||||
disabled={isMutating}
|
||||
disabled={isMutating || isCuesheet}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
+87
-33
@@ -1,5 +1,5 @@
|
||||
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 { maybeAxiosError, unwrapError } from '../../../../../common/api/utils';
|
||||
@@ -12,6 +12,7 @@ import { preventEscape } from '../../../../../common/utils/keyEvent';
|
||||
import { isUrlSafe } from '../../../../../common/utils/regex';
|
||||
import { enDash } from '../../../../../common/utils/styleUtils';
|
||||
import { generateUrlPresetOptions } from '../../../../../common/utils/urlPresets';
|
||||
import CuesheetLinkOptions, { CuesheetPermissionValues } from '../../../../sharing/composite/CuesheetLinkOptions';
|
||||
import * as Panel from '../../../panel-utils/PanelUtils';
|
||||
|
||||
import style from './URLPresetForm.module.scss';
|
||||
@@ -62,12 +63,42 @@ export default function URLPresetForm({ urlPreset, onClose }: URLPresetFormProps
|
||||
});
|
||||
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) => {
|
||||
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) {
|
||||
await updatePreset(urlPreset.alias, data);
|
||||
await updatePreset(urlPreset.alias, payload);
|
||||
} else {
|
||||
await addPreset(data);
|
||||
await addPreset(payload);
|
||||
}
|
||||
onClose();
|
||||
} catch (error) {
|
||||
@@ -75,6 +106,7 @@ export default function URLPresetForm({ urlPreset, onClose }: URLPresetFormProps
|
||||
}
|
||||
};
|
||||
|
||||
// focus on alias when the form opens
|
||||
useEffect(() => {
|
||||
setFocus('alias');
|
||||
}, [setFocus]);
|
||||
@@ -127,44 +159,66 @@ export default function URLPresetForm({ urlPreset, onClose }: URLPresetFormProps
|
||||
<div className={style.expand}>
|
||||
<Panel.Description>Generate options (paste URL to generate options)</Panel.Description>
|
||||
<Panel.InlineElements>
|
||||
<Input placeholder='Paste URL' fluid ref={urlRef} />
|
||||
<Button onClick={generateOptions}>Generate</Button>
|
||||
<Input placeholder='Paste URL' fluid ref={urlRef} disabled={isEditingCuesheet} />
|
||||
<Button onClick={generateOptions} disabled={isEditingCuesheet}>
|
||||
Generate
|
||||
</Button>
|
||||
</Panel.InlineElements>
|
||||
</div>
|
||||
</Panel.InlineElements>
|
||||
{errors.alias?.message && <Panel.Error>{errors.alias.message}</Panel.Error>}
|
||||
<div>
|
||||
{enDash} or {enDash}
|
||||
</div>
|
||||
<div>2. Choose a view and its parameters</div>
|
||||
<div>
|
||||
<Panel.Description>Target</Panel.Description>
|
||||
<Select
|
||||
options={targetOptions}
|
||||
{...register('target', { required: 'Target is required' })}
|
||||
value={watch('target')}
|
||||
onValueChange={(value: OntimeViewPresettable | null) => {
|
||||
if (value === null) return;
|
||||
setValue('target', value, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Panel.Description>Parameters</Panel.Description>
|
||||
<Textarea
|
||||
fluid
|
||||
rows={3}
|
||||
{...register('search', {
|
||||
validate: validateParams,
|
||||
})}
|
||||
/>
|
||||
<Panel.Error>{errors.search?.message}</Panel.Error>
|
||||
</div>
|
||||
{!isEditingCuesheet && (
|
||||
<>
|
||||
<div>
|
||||
{enDash} or {enDash}
|
||||
</div>
|
||||
<div>2. Choose a view and its parameters</div>
|
||||
<div>
|
||||
<Panel.Description>Target</Panel.Description>
|
||||
<Select
|
||||
options={targetOptions}
|
||||
{...register('target', { required: 'Target is required' })}
|
||||
value={watch('target')}
|
||||
onValueChange={(value: OntimeViewPresettable | null) => {
|
||||
if (value === null) return;
|
||||
setValue('target', value, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Panel.Description>Parameters</Panel.Description>
|
||||
<Textarea
|
||||
fluid
|
||||
rows={3}
|
||||
{...register('search', {
|
||||
validate: validateParams,
|
||||
})}
|
||||
/>
|
||||
<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>
|
||||
<Panel.Error>{errors.root?.message}</Panel.Error>
|
||||
<Panel.InlineElements align='end'>
|
||||
<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
|
||||
</Button>
|
||||
</Panel.InlineElements>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { OntimeView, URLPreset } from 'ontime-types';
|
||||
import { generateId } from 'ontime-utils';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { FieldErrors, useForm } from 'react-hook-form';
|
||||
|
||||
import { generateUrl } from '../../common/api/session';
|
||||
@@ -18,7 +18,7 @@ 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 from './composite/CuesheetLinkOptions';
|
||||
import CuesheetLinkOptions, { CuesheetPermissionValues } from './composite/CuesheetLinkOptions';
|
||||
|
||||
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) {
|
||||
const [formState, setFormState] = useState<GenerateLinkState>('pending');
|
||||
const [url, setUrl] = useState(serverURL);
|
||||
const cuesheetReadRef = useRef<HTMLInputElement>(null);
|
||||
const cuesheetWriteRef = useRef<HTMLInputElement>(null);
|
||||
const [url, setUrl] = useState('');
|
||||
const [cuesheetPermissions, setCuesheetPermissions] = useState<CuesheetPermissionValues>({
|
||||
read: 'full',
|
||||
write: 'full',
|
||||
});
|
||||
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 {
|
||||
handleSubmit,
|
||||
@@ -90,7 +104,7 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, presets, is
|
||||
if (options.read === '-') {
|
||||
throw new Error('Cannot create a share with no read permissions');
|
||||
}
|
||||
const presets = await addPreset({
|
||||
const payload = {
|
||||
target: OntimeView.Cuesheet,
|
||||
enabled: true,
|
||||
alias,
|
||||
@@ -100,7 +114,10 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, presets, is
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -109,8 +126,8 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, presets, is
|
||||
setFormState('loading');
|
||||
if (options.path === OntimeView.Cuesheet) {
|
||||
const urlPreset = await createPresetFromOptions((options as CuesheetLinkOptions).alias, {
|
||||
read: cuesheetReadRef.current?.value ?? 'full',
|
||||
write: cuesheetWriteRef.current?.value ?? 'full',
|
||||
read: cuesheetPermissions.read,
|
||||
write: cuesheetPermissions.write,
|
||||
});
|
||||
|
||||
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';
|
||||
|
||||
return (
|
||||
@@ -221,7 +239,7 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, presets, is
|
||||
})}
|
||||
/>
|
||||
</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.Error>{errors.root?.message}</Panel.Error>
|
||||
<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!'}
|
||||
</Button>
|
||||
</Panel.InlineElements>
|
||||
</div>
|
||||
<Panel.Section className={style.column}>
|
||||
<Panel.Description>Share this link</Panel.Description>
|
||||
<QRCode size={172} value={url} />
|
||||
<div className={style.copiableLink} data-testid='copy-link'>
|
||||
{url}
|
||||
</div>
|
||||
<CopyTag copyValue={url}>Copy link</CopyTag>
|
||||
{url ? (
|
||||
<>
|
||||
<QRCode size={172} value={url} />
|
||||
<div className={style.copiableLink} data-testid='copy-link'>
|
||||
{url}
|
||||
</div>
|
||||
<CopyTag copyValue={url}>Copy link</CopyTag>
|
||||
</>
|
||||
) : (
|
||||
<Panel.Description>Your link will appear here once you create it.</Panel.Description>
|
||||
)}
|
||||
</Panel.Section>
|
||||
</div>
|
||||
</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 Switch from '../../../common/components/switch/Switch';
|
||||
@@ -10,118 +10,141 @@ import style from './CuesheetLinkOptions.module.scss';
|
||||
|
||||
type AccessMode = 'full' | 'custom';
|
||||
|
||||
interface CuesheetLinkOptionsProps {
|
||||
readRef?: RefObject<HTMLInputElement | null>;
|
||||
writeRef?: RefObject<HTMLInputElement | null>;
|
||||
export interface CuesheetPermissionValues {
|
||||
read: string;
|
||||
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 customFieldColumns = useMemo(() => makeCuesheetCustomColumns(data), [data]);
|
||||
const allColumns = useMemo(() => [...cuesheetDefaultColumns, ...customFieldColumns], [customFieldColumns]);
|
||||
|
||||
const [readPermissions, setReadPermissions] = useState<AccessMode>('full');
|
||||
const [writePermissions, setWritePermissions] = useState<AccessMode>('full');
|
||||
// Parsed seed values - stable for the lifetime of a given preset
|
||||
const initialReadKeys = useMemo(() => parseKeys(initialRead), [initialRead]);
|
||||
const initialWriteKeys = useMemo(() => parseKeys(initialWrite), [initialWrite]);
|
||||
|
||||
const [readSwitches, setReadSwitches] = useState<Record<string, boolean>>(() => {
|
||||
const initialState: Record<string, boolean> = {};
|
||||
[...cuesheetDefaultColumns, ...customFieldColumns].forEach((column) => {
|
||||
initialState[column.value] = true;
|
||||
const [readPermissions, setReadPermissions] = useState<AccessMode>(() => modeFromPermission(initialRead));
|
||||
const [writePermissions, setWritePermissions] = useState<AccessMode>(() => modeFromPermission(initialWrite));
|
||||
|
||||
// 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;
|
||||
});
|
||||
|
||||
const [writeSwitches, setWriteSwitches] = useState<Record<string, boolean>>(() => {
|
||||
const initialState: Record<string, boolean> = {};
|
||||
[...cuesheetDefaultColumns, ...customFieldColumns].forEach((column) => {
|
||||
initialState[column.value] = true;
|
||||
setWriteSwitches((prev) => {
|
||||
const next = { ...prev };
|
||||
for (const column of allColumns) {
|
||||
if (!(column.value in next)) next[column.value] = defaultWrite(column.value);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
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) => {
|
||||
setReadPermissions(value);
|
||||
|
||||
setReadSwitches((prevReadSwitches) => {
|
||||
const updatedReadSwitches = { ...prevReadSwitches };
|
||||
Object.keys(updatedReadSwitches).forEach((key) => {
|
||||
updatedReadSwitches[key] = true;
|
||||
});
|
||||
return updatedReadSwitches;
|
||||
});
|
||||
};
|
||||
|
||||
const handleWriteModeChange = (value: AccessMode) => {
|
||||
setWritePermissions(value);
|
||||
// Full write implies full read
|
||||
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 handleReadSwitch = (key: string, value: boolean) => {
|
||||
setReadSwitches((prev) => ({ ...prev, [key]: value }));
|
||||
// A column the recipient cannot read cannot be written either
|
||||
if (!value) {
|
||||
setWriteSwitches((prev) => ({ ...prev, [key]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
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') {
|
||||
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)
|
||||
.filter(([_, value]) => value)
|
||||
.map(([key]) => key)
|
||||
.join(',');
|
||||
};
|
||||
|
||||
const getWritePermissions = () => {
|
||||
const resolvedWrite = useMemo(() => {
|
||||
if (writePermissions === '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)
|
||||
.filter(([_, value]) => value)
|
||||
.map(([key]) => key)
|
||||
.join(',');
|
||||
};
|
||||
// Notify the parent of the resolved permissions. onChange is expected to be stable.
|
||||
useEffect(() => {
|
||||
onChange({ read: resolvedRead, write: resolvedWrite });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [resolvedRead, resolvedWrite]);
|
||||
|
||||
const noReadAccess = resolvedRead === '-';
|
||||
|
||||
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' />
|
||||
<Panel.Field title='Access mode' description='Which parts of the data the link gives 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}
|
||||
@@ -132,8 +155,18 @@ export default function CuesheetLinkOptions({ readRef, writeRef }: CuesheetLinkO
|
||||
{ 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>
|
||||
{noReadAccess && <Panel.Error>Links must contain at least one readable column.</Panel.Error>}
|
||||
<div className={style.twoCols}>
|
||||
<div className={style.grid}>
|
||||
<Panel.Description>Ontime columns</Panel.Description>
|
||||
@@ -143,14 +176,14 @@ export default function CuesheetLinkOptions({ readRef, writeRef }: CuesheetLinkO
|
||||
<Fragment key={column.value}>
|
||||
<div>{column.label}</div>
|
||||
<Switch
|
||||
checked={Boolean(readSwitches[column.value])}
|
||||
onCheckedChange={(value: boolean) => handleSwitchChange(column.value, 'read', value)}
|
||||
checked={isReadOn(column.value)}
|
||||
onCheckedChange={(value: boolean) => handleReadSwitch(column.value, 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)}
|
||||
checked={isWriteOn(column.value)}
|
||||
onCheckedChange={(value: boolean) => handleWriteSwitch(column.value, value)}
|
||||
disabled={writePermissions === 'full'}
|
||||
data-testid={`write-${column.value}`}
|
||||
/>
|
||||
@@ -164,16 +197,16 @@ export default function CuesheetLinkOptions({ readRef, writeRef }: CuesheetLinkO
|
||||
<Panel.Description>Write</Panel.Description>
|
||||
{customFieldColumns.map((column) => (
|
||||
<Fragment key={column.value}>
|
||||
{column.label}
|
||||
<div>{column.label}</div>
|
||||
<Switch
|
||||
checked={Boolean(readSwitches[column.value])}
|
||||
onCheckedChange={(value: boolean) => handleSwitchChange(column.value, 'read', value)}
|
||||
checked={isReadOn(column.value)}
|
||||
onCheckedChange={(value: boolean) => handleReadSwitch(column.value, 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)}
|
||||
checked={isWriteOn(column.value)}
|
||||
onCheckedChange={(value: boolean) => handleWriteSwitch(column.value, value)}
|
||||
disabled={writePermissions === 'full'}
|
||||
data-testid={`write-${column.value}`}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user