mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-24 16:39:13 +00:00
refactor: extract cuesheet permissions
This commit is contained in:
committed by
Carlos Valente
parent
5be504ce8f
commit
df588b8845
@@ -1,49 +1,19 @@
|
|||||||
import { memo, use, useEffect, useMemo } from 'react';
|
import { memo, use, useMemo } from 'react';
|
||||||
import { useSessionStorage } from '@mantine/hooks';
|
|
||||||
|
|
||||||
import EmptyPage from '../../common/components/state/EmptyPage';
|
import EmptyPage from '../../common/components/state/EmptyPage';
|
||||||
import { PresetContext } from '../../common/context/PresetContext';
|
import { PresetContext } from '../../common/context/PresetContext';
|
||||||
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
||||||
import { sessionScope } from '../../externals';
|
|
||||||
import { AppMode, sessionKeys } from '../../ontimeConfig';
|
|
||||||
|
|
||||||
import CuesheetDnd from './cuesheet-dnd/CuesheetDnd';
|
import CuesheetDnd from './cuesheet-dnd/CuesheetDnd';
|
||||||
import { makeCuesheetColumns } from './cuesheet-table/cuesheet-table-elements/cuesheetColsFactory';
|
import { makeCuesheetColumns } from './cuesheet-table/cuesheet-table-elements/cuesheetColsFactory';
|
||||||
import CuesheetTable from './cuesheet-table/CuesheetTable';
|
import CuesheetTable from './cuesheet-table/CuesheetTable';
|
||||||
import { useCuesheetPermissions } from './useTablePermissions';
|
import { useApplyCuesheetPolicy } from './useApplyCuesheetPolicy';
|
||||||
|
|
||||||
export default memo(CuesheetTableWrapper);
|
export default memo(CuesheetTableWrapper);
|
||||||
function CuesheetTableWrapper() {
|
function CuesheetTableWrapper() {
|
||||||
const { data: customFields, status: customFieldStatus } = useCustomFields();
|
const { data: customFields, status: customFieldStatus } = useCustomFields();
|
||||||
const setPermissions = useCuesheetPermissions((state) => state.setPermissions);
|
|
||||||
const preset = use(PresetContext);
|
const preset = use(PresetContext);
|
||||||
|
const { cuesheetMode, setCuesheetMode } = useApplyCuesheetPolicy(preset);
|
||||||
// set permissions based on preset
|
|
||||||
useEffect(() => {
|
|
||||||
if (preset) {
|
|
||||||
const fullWrite = preset.options?.write === 'full';
|
|
||||||
setPermissions({
|
|
||||||
canChangeMode: preset.options?.write !== '-',
|
|
||||||
canCreateEntries: fullWrite,
|
|
||||||
canEditEntries: fullWrite,
|
|
||||||
canFlag: fullWrite || Boolean(preset.options?.write.includes('flag')),
|
|
||||||
canShare: false, // TODO: should be sessionScope === 'rw' when we have granular scopes
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setPermissions({
|
|
||||||
canChangeMode: true,
|
|
||||||
canCreateEntries: true,
|
|
||||||
canEditEntries: true,
|
|
||||||
canFlag: true,
|
|
||||||
canShare: sessionScope === 'rw',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [preset, setPermissions]);
|
|
||||||
|
|
||||||
const [cuesheetMode] = useSessionStorage({
|
|
||||||
key: preset ? `${preset.alias}${sessionKeys.cuesheetMode}` : sessionKeys.cuesheetMode,
|
|
||||||
defaultValue: preset ? AppMode.Run : AppMode.Edit,
|
|
||||||
});
|
|
||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() => makeCuesheetColumns(customFields, cuesheetMode, preset),
|
() => makeCuesheetColumns(customFields, cuesheetMode, preset),
|
||||||
@@ -54,7 +24,16 @@ function CuesheetTableWrapper() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<CuesheetDnd columns={columns}>
|
<CuesheetDnd columns={columns}>
|
||||||
{isLoading ? <EmptyPage text='Loading...' /> : <CuesheetTable columns={columns} cuesheetMode={cuesheetMode} />}
|
{isLoading ? (
|
||||||
|
<EmptyPage text='Loading...' />
|
||||||
|
) : (
|
||||||
|
<CuesheetTable
|
||||||
|
columns={columns}
|
||||||
|
cuesheetMode={cuesheetMode}
|
||||||
|
tableRoot='cuesheet'
|
||||||
|
setCuesheetMode={setCuesheetMode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</CuesheetDnd>
|
</CuesheetDnd>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { OntimeView, URLPreset } from 'ontime-types';
|
||||||
|
|
||||||
|
import { AppMode } from '../../../ontimeConfig';
|
||||||
|
import { getCuesheetColumnAccessPolicy, getCuesheetPermissionsPolicy } from '../cuesheet.policies';
|
||||||
|
|
||||||
|
describe('getCuesheetPermissionsPolicy()', () => {
|
||||||
|
test('returns full permissions when there is no preset', () => {
|
||||||
|
expect(getCuesheetPermissionsPolicy(undefined, true)).toEqual({
|
||||||
|
canChangeMode: true,
|
||||||
|
canCreateEntries: true,
|
||||||
|
canEditEntries: true,
|
||||||
|
canFlag: true,
|
||||||
|
canShare: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns run-only permissions when write is disabled', () => {
|
||||||
|
const preset: URLPreset = {
|
||||||
|
enabled: true,
|
||||||
|
alias: 'cuesheet-read-only',
|
||||||
|
target: OntimeView.Cuesheet,
|
||||||
|
search: '',
|
||||||
|
options: {
|
||||||
|
read: 'full',
|
||||||
|
write: '-',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getCuesheetPermissionsPolicy(preset, true)).toEqual({
|
||||||
|
canChangeMode: false,
|
||||||
|
canCreateEntries: false,
|
||||||
|
canEditEntries: false,
|
||||||
|
canFlag: false,
|
||||||
|
canShare: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('allows flag changes when write includes flag only', () => {
|
||||||
|
const preset: URLPreset = {
|
||||||
|
enabled: true,
|
||||||
|
alias: 'cuesheet-flag',
|
||||||
|
target: OntimeView.Cuesheet,
|
||||||
|
search: '',
|
||||||
|
options: {
|
||||||
|
read: 'full',
|
||||||
|
write: 'flag',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getCuesheetPermissionsPolicy(preset, true)).toEqual({
|
||||||
|
canChangeMode: true,
|
||||||
|
canCreateEntries: false,
|
||||||
|
canEditEntries: false,
|
||||||
|
canFlag: true,
|
||||||
|
canShare: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('defaults to full read and write when cuesheet options are absent', () => {
|
||||||
|
const preset: URLPreset = {
|
||||||
|
enabled: true,
|
||||||
|
alias: 'cuesheet-default',
|
||||||
|
target: OntimeView.Cuesheet,
|
||||||
|
search: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const policy = getCuesheetColumnAccessPolicy(preset, AppMode.Edit);
|
||||||
|
|
||||||
|
expect(policy.canRead('title')).toBe(true);
|
||||||
|
expect(policy.canWrite('title')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('column access honors granular permissions and mode', () => {
|
||||||
|
const preset: URLPreset = {
|
||||||
|
enabled: true,
|
||||||
|
alias: 'cuesheet-granular',
|
||||||
|
target: OntimeView.Cuesheet,
|
||||||
|
search: '',
|
||||||
|
options: {
|
||||||
|
read: 'cue,title',
|
||||||
|
write: 'title',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const editPolicy = getCuesheetColumnAccessPolicy(preset, AppMode.Edit);
|
||||||
|
const runPolicy = getCuesheetColumnAccessPolicy(preset, AppMode.Run);
|
||||||
|
|
||||||
|
expect(editPolicy.canRead('cue')).toBe(true);
|
||||||
|
expect(editPolicy.canRead('duration')).toBe(false);
|
||||||
|
expect(editPolicy.canWrite('title')).toBe(true);
|
||||||
|
expect(editPolicy.canWrite('cue')).toBe(false);
|
||||||
|
expect(runPolicy.canWrite('title')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -34,13 +34,24 @@ import { useColumnOrder, useColumnSizes, useColumnVisibility } from './useColumn
|
|||||||
|
|
||||||
import style from './CuesheetTable.module.scss';
|
import style from './CuesheetTable.module.scss';
|
||||||
|
|
||||||
interface CuesheetTableProps {
|
type CuesheetTableBaseProps = {
|
||||||
columns: ColumnDef<ExtendedEntry>[];
|
columns: ColumnDef<ExtendedEntry>[];
|
||||||
cuesheetMode: AppMode;
|
cuesheetMode: AppMode;
|
||||||
tableRoot?: 'editor' | 'cuesheet';
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default function CuesheetTable({ columns, cuesheetMode, tableRoot = 'cuesheet' }: CuesheetTableProps) {
|
type EditorCuesheetTableProps = CuesheetTableBaseProps & {
|
||||||
|
tableRoot: 'editor';
|
||||||
|
setCuesheetMode?: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ViewCuesheetTableProps = CuesheetTableBaseProps & {
|
||||||
|
tableRoot: 'cuesheet';
|
||||||
|
setCuesheetMode: (mode: AppMode) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CuesheetTableProps = EditorCuesheetTableProps | ViewCuesheetTableProps;
|
||||||
|
|
||||||
|
export default function CuesheetTable({ columns, cuesheetMode, tableRoot, setCuesheetMode }: CuesheetTableProps) {
|
||||||
const { data, status } = useFlatRundownWithMetadata();
|
const { data, status } = useFlatRundownWithMetadata();
|
||||||
const { updateEntry, updateTimer } = useEntryActionsContext();
|
const { updateEntry, updateTimer } = useEntryActionsContext();
|
||||||
|
|
||||||
@@ -206,17 +217,25 @@ export default function CuesheetTable({ columns, cuesheetMode, tableRoot = 'cues
|
|||||||
return <EmptyPage text='Loading...' />;
|
return <EmptyPage text='Loading...' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// control components need different implementations for handling permissions
|
|
||||||
const TableRootSettings = tableRoot === 'editor' ? EditorTableSettings : CuesheetTableSettings;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TableRootSettings
|
{tableRoot === 'editor' ? (
|
||||||
columns={allLeafColumns}
|
<EditorTableSettings
|
||||||
handleResetResizing={resetColumnResizing}
|
columns={allLeafColumns}
|
||||||
handleResetReordering={resetColumnOrder}
|
handleResetResizing={resetColumnResizing}
|
||||||
handleClearToggles={setAllVisible}
|
handleResetReordering={resetColumnOrder}
|
||||||
/>
|
handleClearToggles={setAllVisible}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CuesheetTableSettings
|
||||||
|
columns={allLeafColumns}
|
||||||
|
cuesheetMode={cuesheetMode}
|
||||||
|
setCuesheetMode={setCuesheetMode}
|
||||||
|
handleResetResizing={resetColumnResizing}
|
||||||
|
handleResetReordering={resetColumnOrder}
|
||||||
|
handleClearToggles={setAllVisible}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<TableVirtuoso
|
<TableVirtuoso
|
||||||
ref={virtuosoRef}
|
ref={virtuosoRef}
|
||||||
data={data}
|
data={data}
|
||||||
|
|||||||
+2
-9
@@ -7,6 +7,7 @@ import DelayIndicator from '../../../../common/components/delay-indicator/DelayI
|
|||||||
import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata';
|
import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata';
|
||||||
import { formatDuration, formatTime } from '../../../../common/utils/time';
|
import { formatDuration, formatTime } from '../../../../common/utils/time';
|
||||||
import { AppMode } from '../../../../ontimeConfig';
|
import { AppMode } from '../../../../ontimeConfig';
|
||||||
|
import { getCuesheetColumnAccessPolicy } from '../../cuesheet.policies';
|
||||||
|
|
||||||
import DurationInput from './DurationInput';
|
import DurationInput from './DurationInput';
|
||||||
import EditableImage from './EditableImage';
|
import EditableImage from './EditableImage';
|
||||||
@@ -257,15 +258,7 @@ export function makeCuesheetColumns(
|
|||||||
preset: URLPreset | undefined,
|
preset: URLPreset | undefined,
|
||||||
): ColumnDef<ExtendedEntry>[] {
|
): ColumnDef<ExtendedEntry>[] {
|
||||||
const columnsDef: ColumnDef<ExtendedEntry>[] = [];
|
const columnsDef: ColumnDef<ExtendedEntry>[] = [];
|
||||||
const modeAllowsWrite = cuesheetMode === AppMode.Edit;
|
const { canRead, canWrite } = getCuesheetColumnAccessPolicy(preset, cuesheetMode);
|
||||||
const fullRead = preset ? preset.options?.read === 'full' : true;
|
|
||||||
const fullWrite = preset ? preset.options?.write === 'full' : true;
|
|
||||||
const canWriteKeys = preset?.options?.write ? new Set(preset.options.write.split(',')) : new Set<string>();
|
|
||||||
const canReadKeys = preset?.options?.read ? new Set(preset.options.read.split(',')) : new Set<string>();
|
|
||||||
|
|
||||||
// helpers to check read/write for a given key
|
|
||||||
const canRead = (key: string) => fullRead || canReadKeys.has(key);
|
|
||||||
const canWrite = (key: string) => modeAllowsWrite && (fullWrite || canWriteKeys.has(key));
|
|
||||||
|
|
||||||
if (canRead('flag')) {
|
if (canRead('flag')) {
|
||||||
columnsDef.push({
|
columnsDef.push({
|
||||||
|
|||||||
+6
-10
@@ -1,20 +1,18 @@
|
|||||||
import { ReactNode, use } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { IoBookOutline, IoChevronDown, IoOptions } from 'react-icons/io5';
|
import { IoBookOutline, IoChevronDown, IoOptions } from 'react-icons/io5';
|
||||||
import { Popover } from '@base-ui/react/popover';
|
import { Popover } from '@base-ui/react/popover';
|
||||||
import { Toggle } from '@base-ui/react/toggle';
|
import { Toggle } from '@base-ui/react/toggle';
|
||||||
import { ToggleGroup } from '@base-ui/react/toggle-group';
|
import { ToggleGroup } from '@base-ui/react/toggle-group';
|
||||||
import { Toolbar } from '@base-ui/react/toolbar';
|
import { Toolbar } from '@base-ui/react/toolbar';
|
||||||
import { useSessionStorage } from '@mantine/hooks';
|
|
||||||
import type { Column } from '@tanstack/react-table';
|
import type { Column } from '@tanstack/react-table';
|
||||||
|
|
||||||
import Button from '../../../../common/components/buttons/Button';
|
import Button from '../../../../common/components/buttons/Button';
|
||||||
import Checkbox from '../../../../common/components/checkbox/Checkbox';
|
import Checkbox from '../../../../common/components/checkbox/Checkbox';
|
||||||
import * as Editor from '../../../../common/components/editor-utils/EditorUtils';
|
import * as Editor from '../../../../common/components/editor-utils/EditorUtils';
|
||||||
import PopoverContents from '../../../../common/components/popover/Popover';
|
import PopoverContents from '../../../../common/components/popover/Popover';
|
||||||
import { PresetContext } from '../../../../common/context/PresetContext';
|
|
||||||
import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata';
|
import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata';
|
||||||
import { cx } from '../../../../common/utils/styleUtils';
|
import { cx } from '../../../../common/utils/styleUtils';
|
||||||
import { AppMode, sessionKeys } from '../../../../ontimeConfig';
|
import { AppMode } from '../../../../ontimeConfig';
|
||||||
import { CuesheetOptions, usePersistedCuesheetOptions } from '../../cuesheet.options';
|
import { CuesheetOptions, usePersistedCuesheetOptions } from '../../cuesheet.options';
|
||||||
import { useCuesheetPermissions } from '../../useTablePermissions';
|
import { useCuesheetPermissions } from '../../useTablePermissions';
|
||||||
|
|
||||||
@@ -24,6 +22,8 @@ import style from './CuesheetTableSettings.module.scss';
|
|||||||
|
|
||||||
interface CuesheetTableSettingsProps {
|
interface CuesheetTableSettingsProps {
|
||||||
columns: Column<ExtendedEntry, unknown>[];
|
columns: Column<ExtendedEntry, unknown>[];
|
||||||
|
cuesheetMode: AppMode;
|
||||||
|
setCuesheetMode: (mode: AppMode) => void;
|
||||||
handleResetResizing: () => void;
|
handleResetResizing: () => void;
|
||||||
handleResetReordering: () => void;
|
handleResetReordering: () => void;
|
||||||
handleClearToggles: () => void;
|
handleClearToggles: () => void;
|
||||||
@@ -42,20 +42,16 @@ export interface ColumnSettingsProps {
|
|||||||
|
|
||||||
export default function CuesheetTableSettings({
|
export default function CuesheetTableSettings({
|
||||||
columns,
|
columns,
|
||||||
|
cuesheetMode,
|
||||||
|
setCuesheetMode,
|
||||||
handleResetResizing,
|
handleResetResizing,
|
||||||
handleResetReordering,
|
handleResetReordering,
|
||||||
handleClearToggles,
|
handleClearToggles,
|
||||||
}: CuesheetTableSettingsProps) {
|
}: CuesheetTableSettingsProps) {
|
||||||
const canChangeMode = useCuesheetPermissions((state) => state.canChangeMode);
|
const canChangeMode = useCuesheetPermissions((state) => state.canChangeMode);
|
||||||
const canShare = useCuesheetPermissions((state) => state.canShare);
|
const canShare = useCuesheetPermissions((state) => state.canShare);
|
||||||
const preset = use(PresetContext);
|
|
||||||
const options = usePersistedCuesheetOptions();
|
const options = usePersistedCuesheetOptions();
|
||||||
|
|
||||||
const [cuesheetMode, setCuesheetMode] = useSessionStorage({
|
|
||||||
key: preset ? `${preset.alias}${sessionKeys.cuesheetMode}` : sessionKeys.cuesheetMode,
|
|
||||||
defaultValue: preset ? AppMode.Run : AppMode.Edit,
|
|
||||||
});
|
|
||||||
|
|
||||||
const toggleCuesheetMode = (mode: AppMode[]) => {
|
const toggleCuesheetMode = (mode: AppMode[]) => {
|
||||||
// we need to stop user from deselecting a mode
|
// we need to stop user from deselecting a mode
|
||||||
const newValue = mode.at(0);
|
const newValue = mode.at(0);
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { URLPreset } from 'ontime-types';
|
||||||
|
|
||||||
|
import { AppMode } from '../../ontimeConfig';
|
||||||
|
|
||||||
|
import type { CuesheetPermissions } from './useTablePermissions';
|
||||||
|
|
||||||
|
function getPermissionKeys(permission: string | undefined): Set<string> {
|
||||||
|
return permission ? new Set(permission.split(',')) : new Set<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCuesheetPermissions(readPermission: string | undefined, writePermission: string | undefined) {
|
||||||
|
const readKeys = getPermissionKeys(readPermission);
|
||||||
|
const writeKeys = getPermissionKeys(writePermission);
|
||||||
|
const fullRead = readPermission == null || readPermission === 'full';
|
||||||
|
const fullWrite = writePermission == null || writePermission === 'full';
|
||||||
|
|
||||||
|
return {
|
||||||
|
writePermission,
|
||||||
|
readKeys,
|
||||||
|
writeKeys,
|
||||||
|
fullRead,
|
||||||
|
fullWrite,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCuesheetPermissionsPolicy(
|
||||||
|
preset: URLPreset | undefined,
|
||||||
|
canShareInSession: boolean,
|
||||||
|
): CuesheetPermissions {
|
||||||
|
if (!preset) {
|
||||||
|
return {
|
||||||
|
canChangeMode: true,
|
||||||
|
canCreateEntries: true,
|
||||||
|
canEditEntries: true,
|
||||||
|
canFlag: true,
|
||||||
|
canShare: canShareInSession,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { writePermission, writeKeys, fullWrite } = getCuesheetPermissions(
|
||||||
|
preset?.options?.read,
|
||||||
|
preset?.options?.write,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
canChangeMode: writePermission !== '-',
|
||||||
|
canCreateEntries: fullWrite,
|
||||||
|
canEditEntries: fullWrite,
|
||||||
|
canFlag: fullWrite || writeKeys.has('flag'),
|
||||||
|
canShare: false, // TODO: should be sessionScope === 'rw' when we have granular scopes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCuesheetColumnAccessPolicy(preset: URLPreset | undefined, cuesheetMode: AppMode) {
|
||||||
|
const { readKeys, writeKeys, fullRead, fullWrite } = getCuesheetPermissions(
|
||||||
|
preset?.options?.read,
|
||||||
|
preset?.options?.write,
|
||||||
|
);
|
||||||
|
const modeAllowsWrite = cuesheetMode === AppMode.Edit;
|
||||||
|
|
||||||
|
return {
|
||||||
|
canRead: (key: string) => fullRead || readKeys.has(key),
|
||||||
|
canWrite: (key: string) => modeAllowsWrite && (fullWrite || writeKeys.has(key)),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
import { useSessionStorage } from '@mantine/hooks';
|
||||||
|
import { URLPreset } from 'ontime-types';
|
||||||
|
|
||||||
|
import { sessionScope } from '../../externals';
|
||||||
|
import { AppMode, sessionKeys } from '../../ontimeConfig';
|
||||||
|
|
||||||
|
import { getCuesheetPermissionsPolicy } from './cuesheet.policies';
|
||||||
|
import { useCuesheetPermissions } from './useTablePermissions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies cuesheet permissions to shared state and exposes the effective mode for the UI.
|
||||||
|
*/
|
||||||
|
export function useApplyCuesheetPolicy(preset: URLPreset | undefined): {
|
||||||
|
cuesheetMode: AppMode;
|
||||||
|
setCuesheetMode: (mode: AppMode) => void;
|
||||||
|
} {
|
||||||
|
const setPermissions = useCuesheetPermissions((state) => state.setPermissions);
|
||||||
|
const canShareInSession = sessionScope === 'rw';
|
||||||
|
const permissions = useMemo(
|
||||||
|
() => getCuesheetPermissionsPolicy(preset, canShareInSession),
|
||||||
|
[preset, canShareInSession],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [storedCuesheetMode, setStoredCuesheetMode] = useSessionStorage({
|
||||||
|
key: preset ? `${preset.alias}${sessionKeys.cuesheetMode}` : sessionKeys.cuesheetMode,
|
||||||
|
defaultValue: preset ? AppMode.Run : AppMode.Edit,
|
||||||
|
});
|
||||||
|
|
||||||
|
const cuesheetMode = permissions.canChangeMode ? storedCuesheetMode : AppMode.Run;
|
||||||
|
const setCuesheetMode = useCallback(
|
||||||
|
(mode: AppMode) => {
|
||||||
|
if (!permissions.canChangeMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStoredCuesheetMode(mode);
|
||||||
|
},
|
||||||
|
[permissions.canChangeMode, setStoredCuesheetMode],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Keep the shared permissions store aligned with the active preset policy.
|
||||||
|
useEffect(() => {
|
||||||
|
setPermissions(permissions);
|
||||||
|
}, [permissions, setPermissions]);
|
||||||
|
|
||||||
|
// Force Run mode whenever the policy forbids mode switching.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!permissions.canChangeMode) {
|
||||||
|
setStoredCuesheetMode((mode) => (mode === AppMode.Run ? mode : AppMode.Run));
|
||||||
|
}
|
||||||
|
}, [permissions.canChangeMode, setStoredCuesheetMode]);
|
||||||
|
|
||||||
|
return { cuesheetMode, setCuesheetMode };
|
||||||
|
}
|
||||||
@@ -6,9 +6,11 @@ interface CuesheetPermissionsStore {
|
|||||||
canEditEntries: boolean;
|
canEditEntries: boolean;
|
||||||
canFlag: boolean;
|
canFlag: boolean;
|
||||||
canShare: boolean;
|
canShare: boolean;
|
||||||
setPermissions: (permissions: Omit<CuesheetPermissionsStore, 'setPermissions'>) => void;
|
setPermissions: (permissions: CuesheetPermissions) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CuesheetPermissions = Omit<CuesheetPermissionsStore, 'setPermissions'>;
|
||||||
|
|
||||||
export const useCuesheetPermissions = create<CuesheetPermissionsStore>((set) => ({
|
export const useCuesheetPermissions = create<CuesheetPermissionsStore>((set) => ({
|
||||||
canChangeMode: false,
|
canChangeMode: false,
|
||||||
canCreateEntries: false,
|
canCreateEntries: false,
|
||||||
|
|||||||
@@ -132,14 +132,16 @@ test.describe('Sharing from cuesheet', () => {
|
|||||||
await page.close();
|
await page.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Sharing a link with readonly permissions', async ({ page }) => {
|
test('Sharing a link with readonly permissions', async ({ page }, testInfo) => {
|
||||||
|
const alias = `cuesheet-read-test-${testInfo.retry}-${Date.now()}`;
|
||||||
|
|
||||||
await page.goto('http://localhost:4001/cuesheet');
|
await page.goto('http://localhost:4001/cuesheet');
|
||||||
await expect(page.getByTestId('cuesheet')).toBeVisible();
|
await expect(page.getByTestId('cuesheet')).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Share...' }).click();
|
await page.getByRole('button', { name: 'Share...' }).click();
|
||||||
|
|
||||||
// configure share for readonly
|
// configure share for readonly
|
||||||
await page.getByRole('textbox').fill('cuesheet-read-test');
|
await page.locator('input[name="alias"]').fill(alias);
|
||||||
await page.getByText('Custom write').click();
|
await page.getByText('Custom write').click();
|
||||||
await page.getByText('Custom read').click();
|
await page.getByText('Custom read').click();
|
||||||
await page.getByTestId('lockNav').click();
|
await page.getByTestId('lockNav').click();
|
||||||
@@ -151,9 +153,16 @@ test.describe('Sharing from cuesheet', () => {
|
|||||||
await page.getByTestId('write-duration').click();
|
await page.getByTestId('write-duration').click();
|
||||||
await page.getByTestId('write-note').click();
|
await page.getByTestId('write-note').click();
|
||||||
|
|
||||||
|
// disable any custom write permissions if they exist
|
||||||
|
const customWriteSwitches = page.locator('[data-testid^="write-custom-"]');
|
||||||
|
const customWriteSwitchCount = await customWriteSwitches.count();
|
||||||
|
for (let i = 0; i < customWriteSwitchCount; i++) {
|
||||||
|
await customWriteSwitches.nth(i).click();
|
||||||
|
}
|
||||||
|
|
||||||
// create and verify link
|
// create and verify link
|
||||||
await page.getByRole('button', { name: 'Create share link' }).click();
|
await page.getByRole('button', { name: 'Create share link' }).click();
|
||||||
await expect(page.getByTestId('copy-link')).toContainText('preset/cuesheet-read-test');
|
await expect(page.getByTestId('copy-link')).toContainText(`preset/${alias}`);
|
||||||
await expect(page.getByTestId('copy-link')).toContainText('n=1');
|
await expect(page.getByTestId('copy-link')).toContainText('n=1');
|
||||||
|
|
||||||
// verify the preset
|
// verify the preset
|
||||||
@@ -177,14 +186,16 @@ test.describe('Sharing from cuesheet', () => {
|
|||||||
await expect(page.getByRole('cell', { name: 'Duration' })).toBeVisible();
|
await expect(page.getByRole('cell', { name: 'Duration' })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Sharing a link with scoped read-write permissions', async ({ page }) => {
|
test('Sharing a link with scoped read-write permissions', async ({ page }, testInfo) => {
|
||||||
|
const alias = `cuesheet-scope-test-${testInfo.retry}-${Date.now()}`;
|
||||||
|
|
||||||
await page.goto('http://localhost:4001/cuesheet');
|
await page.goto('http://localhost:4001/cuesheet');
|
||||||
await expect(page.getByTestId('cuesheet')).toBeVisible();
|
await expect(page.getByTestId('cuesheet')).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Share...' }).click();
|
await page.getByRole('button', { name: 'Share...' }).click();
|
||||||
|
|
||||||
// configure share for readonly
|
// configure share for readonly
|
||||||
await page.getByRole('textbox').fill('cuesheet-scope-test');
|
await page.locator('input[name="alias"]').fill(alias);
|
||||||
await page.getByText('Custom write').click();
|
await page.getByText('Custom write').click();
|
||||||
await page.getByText('Custom read').click();
|
await page.getByText('Custom read').click();
|
||||||
await page.getByTestId('lockNav').click();
|
await page.getByTestId('lockNav').click();
|
||||||
@@ -203,7 +214,7 @@ test.describe('Sharing from cuesheet', () => {
|
|||||||
|
|
||||||
// create and verify link
|
// create and verify link
|
||||||
await page.getByRole('button', { name: 'Create share link' }).click();
|
await page.getByRole('button', { name: 'Create share link' }).click();
|
||||||
await expect(page.getByTestId('copy-link')).toContainText('preset/cuesheet-scope-test');
|
await expect(page.getByTestId('copy-link')).toContainText(`preset/${alias}`);
|
||||||
await expect(page.getByTestId('copy-link')).toContainText('n=1');
|
await expect(page.getByTestId('copy-link')).toContainText('n=1');
|
||||||
|
|
||||||
// verify the preset
|
// verify the preset
|
||||||
@@ -220,8 +231,9 @@ test.describe('Sharing from cuesheet', () => {
|
|||||||
|
|
||||||
// Verify that the title is visible and editable
|
// Verify that the title is visible and editable
|
||||||
await expect(page.getByTestId('cuesheet-event').getByRole('cell', { name: 'title' })).toBeVisible();
|
await expect(page.getByTestId('cuesheet-event').getByRole('cell', { name: 'title' })).toBeVisible();
|
||||||
await page.getByTestId('cuesheet-event').getByTestId('cuesheet-editor-title').click();
|
const titleEditor = page.getByTestId('cuesheet-event').getByTestId('cuesheet-editor-title');
|
||||||
await expect(page.getByTestId('cuesheet-event').getByTestId('cuesheet-editor-title').locator('input')).toBeVisible();
|
await titleEditor.click();
|
||||||
|
await expect(titleEditor).toBeEditable();
|
||||||
|
|
||||||
// other elements are not there
|
// other elements are not there
|
||||||
await expect(page.getByRole('cell', { name: 'Duration' })).toBeHidden();
|
await expect(page.getByRole('cell', { name: 'Duration' })).toBeHidden();
|
||||||
|
|||||||
Reference in New Issue
Block a user