Compare commits

...

24 Commits

Author SHA1 Message Date
Carlos Valente a63fa68d39 bump version to 3.3.3 2024-06-23 23:02:14 +02:00
Carlos Valente 20503de6fd refactor: minor code cleanups 2024-06-23 22:07:34 +02:00
Carlos Valente 0335da9786 refactor: improve errors to user 2024-06-23 22:07:34 +02:00
Carlos Valente 9f1a64f53c chore: upgrade dependencies 2024-06-23 22:07:34 +02:00
Alex Christoffer Rasmussen 975356f4f4 events with 0 duration dose not contribute to next day calculation (#1099) 2024-06-23 22:06:44 +02:00
Carlos Valente 1b448835cd fix: duplicate validation 2024-06-23 16:17:03 +02:00
Carlos Valente cbf3d566b9 fix: prevent calling service before init 2024-06-23 11:50:49 +02:00
Carlos Valente 6c20cb5e99 refactor: remove size limits in title 2024-06-22 13:48:19 +02:00
Carlos Valente cfc3aab893 style: add background to highlight 2024-06-21 14:45:45 +02:00
Alex Christoffer Rasmussen b7c48f38a9 hotfix-build (#1091)
* skip  "alwaysStrict": true

* bump to 3.3.2

* remove all new ts check to be sure
2024-06-17 11:44:44 +02:00
Carlos Valente dcb6bfa62d bump version to 3.3.1 2024-06-17 08:21:01 +02:00
Carlos Valente 0abc4de3cd fix: column reordering in cuesheet 2024-06-17 08:14:12 +02:00
Carlos Valente f750ed3cdc style: match table header size 2024-06-17 08:14:12 +02:00
Carlos Valente f026c1c1e3 chore: run linter on imports 2024-06-17 08:10:14 +02:00
Piotr Górski ce176b01a2 Added Polish Translation (#1077) 2024-06-16 14:46:11 +02:00
Carlos Valente 337dd7892a chore: fix typo in donation text 2024-06-16 14:45:16 +02:00
Carlos Valente b7daf2926a refactor: improve typing 2024-06-16 09:07:01 +02:00
jwetzell 81a07099ae Merge pull request #1084 from cpvalente/fix/npm-publish-with-provenance
add permission for provenance publishing
2024-06-15 14:59:28 -05:00
jwetzell 8f88ca5053 add permission for provenance publishing
https://docs.npmjs.com/generating-provenance-statements
2024-06-15 14:59:16 -05:00
jwetzell 82a885dc44 Merge pull request #1082 from cpvalente/fix/publish-action
add registry url for publishing via CI
2024-06-15 13:33:58 -05:00
jwetzell c4be814de5 add registry url for publishing via CI 2024-06-15 13:33:08 -05:00
Carlos Valente 9f3186e249 bump version to 3.3.0 2024-06-15 17:30:58 +02:00
jwetzell 5159554697 Merge pull request #1080 from cpvalente/fix/npm-publish-action
disable git checks for pnpm publish workflow
2024-06-15 08:30:30 -05:00
jwetzell cf690aff08 disable git checks for pnpm publish workflow 2024-06-15 08:25:43 -05:00
53 changed files with 451 additions and 277 deletions
+5 -2
View File
@@ -7,6 +7,8 @@ on:
jobs: jobs:
build_cli: build_cli:
permissions:
id-token: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -19,6 +21,7 @@ jobs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 18.18.2 node-version: 18.18.2
registry-url: 'https://registry.npmjs.org'
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v3 uses: pnpm/action-setup@v3
@@ -43,8 +46,8 @@ jobs:
run: cp -R apps/server/src/external apps/cli/external run: cp -R apps/server/src/external apps/cli/external
- name: Publish to NPM - name: Publish to NPM
run: pnpm publish --access public run: pnpm publish --access public --no-git-checks
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: true NPM_CONFIG_PROVENANCE: true
working-directory: ./apps/cli working-directory: ./apps/cli
+1 -1
View File
@@ -38,7 +38,7 @@ npx install @getontime/cli
## Sponsoring ## Sponsoring
You can help the development of this project or say thank you with a one time donation. \ You can help the development of this project or say thank you with a one time donation. \
See the [terms of fonations](https://github.com/cpvalente/ontime/blob/master/SPONSOR.md) See the [terms of donations](https://github.com/cpvalente/ontime/blob/master/SPONSOR.md)
[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/cpvalente) [![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/cpvalente)
[![](https://img.shields.io/static/v1?label=Buy%20me%20a%20coffee&message=%E2%9D%A4&logo=buymeacoffee&color=%23fe8e86)](https://www.buymeacoffee.com/cpvalente) [![](https://img.shields.io/static/v1?label=Buy%20me%20a%20coffee&message=%E2%9D%A4&logo=buymeacoffee&color=%23fe8e86)](https://www.buymeacoffee.com/cpvalente)
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@getontime/cli", "name": "@getontime/cli",
"version": "3.1.0", "version": "3.3.3",
"author": "Carlos Valente", "author": "Carlos Valente",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime", "repository": "https://github.com/cpvalente/ontime",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime-ui", "name": "ontime-ui",
"version": "3.3.0", "version": "3.3.3",
"private": true, "private": true,
"type": "module", "type": "module",
"dependencies": { "dependencies": {
@@ -1,25 +1,37 @@
@use '../../../theme/viewerDefs' as *; @use '../../../theme/viewerDefs' as *;
.title-card { .title-card {
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.5rem; gap: 0.25rem;
} }
.inline { .title-card__title,
display: flex; .title-card__secondary {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.title-card__title { .title-card__title {
font-weight: 600; font-weight: 600;
font-size: clamp(32px, 3.5vw, 50px); font-size: clamp(1.5rem, 3vw, 3rem);
color: var(--color-override, $viewer-color); color: var(--color-override, $viewer-color);
line-height: 1.1em; line-height: 1.2em;
}
.title-card__secondary {
font-size: clamp(1rem, 2vw, 2.25rem);
color: var(--secondary-color-override, $viewer-secondary-color);
line-height: 1.2em;
} }
.title-card__label { .title-card__label {
position: absolute;
right: 1rem;
top: 0.5rem;
font-size: clamp(1rem, 1.5vw, 1.5rem); font-size: clamp(1rem, 1.5vw, 1.5rem);
font-weight: 400;
color: var(--secondary-color-override, $viewer-secondary-color); color: var(--secondary-color-override, $viewer-secondary-color);
margin-left: auto; margin-left: auto;
text-transform: uppercase; text-transform: uppercase;
@@ -28,13 +40,3 @@
color: var(--accent-color-override, $accent-color); color: var(--accent-color-override, $accent-color);
} }
} }
.title-card__secondary {
font-size: clamp(1.5rem, 2vw, 2.25rem);
color: var(--secondary-color-override, $viewer-secondary-color);
line-height: 1.1em;
&::after {
content: '\200b';
}
}
@@ -1,28 +1,32 @@
import { ForwardedRef, forwardRef } from 'react';
import { useTranslation } from '../../../translation/TranslationProvider'; import { useTranslation } from '../../../translation/TranslationProvider';
import './TitleCard.scss'; import './TitleCard.scss';
interface TitleCardProps { interface TitleCardProps {
label: 'now' | 'next';
title: string; title: string;
label?: 'now' | 'next';
secondary?: string; secondary?: string;
className?: string;
} }
export default function TitleCard(props: TitleCardProps) { const TitleCard = forwardRef((props: TitleCardProps, ref: ForwardedRef<HTMLDivElement>) => {
const { label, title, secondary } = props; const { label, title, secondary, className = '' } = props;
const { getLocalizedString } = useTranslation(); const { getLocalizedString } = useTranslation();
const accent = label === 'now'; const accent = label === 'now';
return ( return (
<div className='title-card'> <div className={`title-card ${className}`} ref={ref}>
<div className='inline'> <span className='title-card__title'>{title}</span>
<span className='title-card__title'>{title}</span> <span className={accent ? 'title-card__label title-card__label--accent' : 'title-card__label'}>
<span className={accent ? 'title-card__label title-card__label--accent' : 'title-card__label'}> {label && getLocalizedString(`common.${label}`)}
{getLocalizedString(`common.${label}`)} </span>
</span>
</div>
<div className='title-card__secondary'>{secondary}</div> <div className='title-card__secondary'>{secondary}</div>
</div> </div>
); );
} });
TitleCard.displayName = 'TitleCard';
export default TitleCard;
@@ -153,6 +153,7 @@ export default function GeneralPanelForm() {
<option value='pt'>Portuguese</option> <option value='pt'>Portuguese</option>
<option value='es'>Spanish</option> <option value='es'>Spanish</option>
<option value='sv'>Swedish</option> <option value='sv'>Swedish</option>
<option value='pl'>Polish</option>
</Select> </Select>
</Panel.ListItem> </Panel.ListItem>
</Panel.ListGroup> </Panel.ListGroup>
@@ -2,8 +2,6 @@ import { useEffect } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { Button, Input } from '@chakra-ui/react'; import { Button, Input } from '@chakra-ui/react';
import * as Panel from '../PanelUtils';
import style from './ProjectPanel.module.scss'; import style from './ProjectPanel.module.scss';
export type ProjectFormValues = { export type ProjectFormValues = {
@@ -15,10 +13,9 @@ interface ProjectFormProps {
filename: string; filename: string;
onCancel: () => void; onCancel: () => void;
onSubmit: (values: ProjectFormValues) => Promise<void>; onSubmit: (values: ProjectFormValues) => Promise<void>;
submitError: string | null;
} }
export default function ProjectForm({ action, filename, onSubmit, onCancel, submitError }: ProjectFormProps) { export default function ProjectForm({ action, filename, onSubmit, onCancel }: ProjectFormProps) {
const { const {
handleSubmit, handleSubmit,
register, register,
@@ -37,34 +34,31 @@ export default function ProjectForm({ action, filename, onSubmit, onCancel, subm
}, [setFocus]); }, [setFocus]);
return ( return (
<> <form onSubmit={handleSubmit(onSubmit)} className={style.form}>
<form onSubmit={handleSubmit(onSubmit)} className={style.form}> <Input
<Input className={style.formInput}
className={style.formInput} id='filename'
id='filename' size='sm'
type='text'
variant='ontime-filled'
placeholder='Enter new name'
autoComplete='off'
{...register('filename', { required: true })}
/>
<div className={style.actionButtons}>
<Button onClick={onCancel} size='sm' variant='ontime-ghosted' disabled={isSubmitting}>
Cancel
</Button>
<Button
size='sm' size='sm'
type='text'
variant='ontime-filled' variant='ontime-filled'
placeholder='Enter new name' isDisabled={!isDirty || !isValid || isSubmitting}
autoComplete='off' type='submit'
{...register('filename')} className={style.saveButton}
/> >
<div className={style.actionButtons}> {action}
<Button onClick={onCancel} size='sm' variant='ontime-ghosted' disabled={isSubmitting}> </Button>
Cancel </div>
</Button> </form>
<Button
size='sm'
variant='ontime-filled'
isDisabled={!isDirty || !isValid || isSubmitting}
type='submit'
className={style.saveButton}
>
{action}
</Button>
</div>
</form>
{submitError && <Panel.Error>{submitError}</Panel.Error>}
</>
); );
} }
@@ -11,6 +11,7 @@ import {
renameProject, renameProject,
} from '../../../../common/api/db'; } from '../../../../common/api/db';
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/utils'; import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/utils';
import * as Panel from '../PanelUtils';
import ProjectForm, { ProjectFormValues } from './ProjectForm'; import ProjectForm, { ProjectFormValues } from './ProjectForm';
@@ -40,36 +41,53 @@ export default function ProjectListItem({
onToggleEditMode, onToggleEditMode,
}: ProjectListItemProps) { }: ProjectListItemProps) {
const [submitError, setSubmitError] = useState<string | null>(null); const [submitError, setSubmitError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const handleSubmitRename = async (values: ProjectFormValues) => { const handleSubmitAction = (actionType: 'rename' | 'duplicate') => {
try { return async (values: ProjectFormValues) => {
setLoading(true);
setSubmitError(null); setSubmitError(null);
try {
if (!values.filename) { if (!values.filename) {
setSubmitError('Filename cannot be blank'); setSubmitError('Filename cannot be blank');
return; return;
}
const action = actionType === 'rename' ? renameProject : duplicateProject;
await action(filename, values.filename);
await onRefetch();
onSubmit();
} catch (error) {
setSubmitError(maybeAxiosError(error));
} finally {
setLoading(false);
} }
await renameProject(filename, values.filename); };
};
const handleLoad = async (filename: string) => {
setLoading(true);
setSubmitError(null);
try {
await loadProject(filename);
await onRefetch(); await onRefetch();
onSubmit(); await invalidateAllCaches();
} catch (error) { } catch (error) {
setSubmitError(maybeAxiosError(error)); setSubmitError(maybeAxiosError(error));
} finally {
setLoading(false);
} }
}; };
const handleSubmitDuplicate = async (values: ProjectFormValues) => { const handleDelete = async (filename: string) => {
setLoading(true);
setSubmitError(null);
try { try {
setSubmitError(null); await deleteProject(filename);
if (!values.filename) {
setSubmitError('Filename cannot be blank');
return;
}
await duplicateProject(filename, values.filename);
await onRefetch(); await onRefetch();
onSubmit();
} catch (error) { } catch (error) {
setSubmitError(maybeAxiosError(error)); setSubmitError(maybeAxiosError(error));
} finally {
setLoading(false);
} }
}; };
@@ -86,50 +104,55 @@ export default function ProjectListItem({
const classes = current && !isCurrentlyBeingEdited ? style.current : undefined; const classes = current && !isCurrentlyBeingEdited ? style.current : undefined;
return ( return (
<tr key={filename} className={classes}> <>
{isCurrentlyBeingEdited ? ( {submitError && (
<td colSpan={99}> <tr key='filename-error'>
<ProjectForm <td colSpan={99}>
action={editingMode} <Panel.Error>{submitError}</Panel.Error>
filename={filename} </td>
onSubmit={editingMode === 'duplicate' ? handleSubmitDuplicate : handleSubmitRename} </tr>
onCancel={handleCancel} )}
submitError={submitError} <tr key={filename} className={classes}>
/> {isCurrentlyBeingEdited ? (
</td> <td colSpan={99}>
) : ( <ProjectForm
<> action={editingMode}
<td className={style.containCell}>{filename}</td>
<td>{new Date(updatedAt).toLocaleString()}</td>
<td className={style.actionButton}>
<ActionMenu
current={current}
filename={filename} filename={filename}
onChangeEditMode={handleToggleEditMode} onSubmit={editingMode === 'duplicate' ? handleSubmitAction('duplicate') : handleSubmitAction('rename')}
onRefetch={onRefetch} onCancel={handleCancel}
/> />
</td> </td>
</> ) : (
)} <>
</tr> <td className={style.containCell}>{filename}</td>
<td>{new Date(updatedAt).toLocaleString()}</td>
<td className={style.actionButton}>
<ActionMenu
current={current}
filename={filename}
onChangeEditMode={handleToggleEditMode}
onDelete={handleDelete}
onLoad={handleLoad}
isDisabled={loading}
/>
</td>
</>
)}
</tr>
</>
); );
} }
function ActionMenu({ interface ActionMenuProps {
current,
filename,
onChangeEditMode,
onRefetch,
}: {
current?: boolean; current?: boolean;
filename: string; filename: string;
isDisabled: boolean;
onChangeEditMode: (editMode: EditMode, filename: string) => void; onChangeEditMode: (editMode: EditMode, filename: string) => void;
onRefetch: () => Promise<void>; onDelete: (filename: string) => void;
}) { onLoad: (filename: string) => void;
const handleLoad = async () => { }
await loadProject(filename); function ActionMenu(props: ActionMenuProps) {
await invalidateAllCaches(); const { current, filename, isDisabled, onChangeEditMode, onDelete, onLoad } = props;
};
const handleRename = () => { const handleRename = () => {
onChangeEditMode('rename', filename); onChangeEditMode('rename', filename);
@@ -139,11 +162,6 @@ function ActionMenu({
onChangeEditMode('duplicate', filename); onChangeEditMode('duplicate', filename);
}; };
const handleDelete = async () => {
await deleteProject(filename);
await onRefetch();
};
const handleDownload = async () => { const handleDownload = async () => {
await downloadProject(filename); await downloadProject(filename);
}; };
@@ -161,16 +179,17 @@ function ActionMenu({
color='#e2e2e2' // $gray-200 color='#e2e2e2' // $gray-200
variant='ontime-ghosted' variant='ontime-ghosted'
size='sm' size='sm'
isDisabled={isDisabled}
/> />
<MenuList> <MenuList>
<MenuItem onClick={handleLoad} isDisabled={current}> <MenuItem onClick={() => onLoad(filename)} isDisabled={current}>
Load Load
</MenuItem> </MenuItem>
<MenuItem onClick={handleRename}>Rename</MenuItem> <MenuItem onClick={handleRename}>Rename</MenuItem>
<MenuItem onClick={handleDuplicate}>Duplicate</MenuItem> <MenuItem onClick={handleDuplicate}>Duplicate</MenuItem>
<MenuItem onClick={handleDownload}>Download</MenuItem> <MenuItem onClick={handleDownload}>Download</MenuItem>
<MenuItem onClick={handleExportCSV}>Export CSV Rundown</MenuItem> <MenuItem onClick={handleExportCSV}>Export CSV Rundown</MenuItem>
<MenuItem isDisabled={current} onClick={handleDelete}> <MenuItem isDisabled={current} onClick={() => onDelete(filename)}>
Delete Delete
</MenuItem> </MenuItem>
</MenuList> </MenuList>
@@ -47,11 +47,13 @@ $table-header-font-size: calc(1rem - 3px);
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 10; z-index: 10;
background-color: $gray-1300;
font-size: $table-header-font-size; font-size: $table-header-font-size;
color: $gray-700; color: $gray-700;
font-weight: 400;
th {
background-color: $gray-1300;
}
} }
.eventRow { .eventRow {
+11 -19
View File
@@ -1,5 +1,4 @@
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useRef } from 'react';
import { useLocalStorage } from '@mantine/hooks';
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'; import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
import Color from 'color'; import Color from 'color';
import { isOntimeBlock, isOntimeDelay, isOntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types'; import { isOntimeBlock, isOntimeDelay, isOntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
@@ -13,7 +12,7 @@ import DelayRow from './cuesheet-table-elements/DelayRow';
import EventRow from './cuesheet-table-elements/EventRow'; import EventRow from './cuesheet-table-elements/EventRow';
import CuesheetTableSettings from './cuesheet-table-settings/CuesheetTableSettings'; import CuesheetTableSettings from './cuesheet-table-settings/CuesheetTableSettings';
import { useCuesheetSettings } from './store/CuesheetSettings'; import { useCuesheetSettings } from './store/CuesheetSettings';
import { initialColumnOrder } from './cuesheetCols'; import useColumnManager from './useColumnManager';
import style from './Cuesheet.module.scss'; import style from './Cuesheet.module.scss';
@@ -27,23 +26,20 @@ interface CuesheetProps {
export default function Cuesheet({ data, columns, handleUpdate, selectedId }: CuesheetProps) { export default function Cuesheet({ data, columns, handleUpdate, selectedId }: CuesheetProps) {
const { followSelected, showSettings, showDelayBlock, showPrevious, showIndexColumn } = useCuesheetSettings(); const { followSelected, showSettings, showDelayBlock, showPrevious, showIndexColumn } = useCuesheetSettings();
const [columnVisibility, setColumnVisibility] = useLocalStorage({ key: 'table-hidden', defaultValue: {} }); const {
const [columnOrder, saveColumnOrder] = useLocalStorage<string[]>({ columnVisibility,
key: 'table-order', columnOrder,
defaultValue: initialColumnOrder, columnSizing,
}); resetColumnOrder,
const [columnSizing, setColumnSizing] = useLocalStorage({ key: 'table-sizes', defaultValue: {} }); setColumnVisibility,
saveColumnOrder,
setColumnSizing,
} = useColumnManager(columns);
const selectedRef = useRef<HTMLTableRowElement | null>(null); const selectedRef = useRef<HTMLTableRowElement | null>(null);
const tableContainerRef = useRef<HTMLDivElement | null>(null); const tableContainerRef = useRef<HTMLDivElement | null>(null);
useFollowComponent({ followRef: selectedRef, scrollRef: tableContainerRef, doFollow: followSelected }); useFollowComponent({ followRef: selectedRef, scrollRef: tableContainerRef, doFollow: followSelected });
// keep column order in sync with columns
useEffect(() => {
const order = columns.map((col) => col.id as string);
saveColumnOrder(order);
}, [columns, saveColumnOrder]);
const table = useReactTable({ const table = useReactTable({
data, data,
columns, columns,
@@ -61,10 +57,6 @@ export default function Cuesheet({ data, columns, handleUpdate, selectedId }: Cu
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
}); });
const resetColumnOrder = () => {
saveColumnOrder(initialColumnOrder);
};
const setAllVisible = () => { const setAllVisible = () => {
table.toggleAllColumnsVisible(true); table.toggleAllColumnsVisible(true);
}; };
@@ -117,5 +117,3 @@ export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<Ontim
...dynamicCustomFields, ...dynamicCustomFields,
]; ];
} }
export const initialColumnOrder: string[] = makeCuesheetColumns({}).map((column) => column.id as string);
@@ -0,0 +1,46 @@
import { useCallback, useEffect } from 'react';
import { useLocalStorage } from '@mantine/hooks';
import { ColumnDef } from '@tanstack/react-table';
import { OntimeRundownEntry } from 'ontime-types';
export default function useColumnManager(columns: ColumnDef<OntimeRundownEntry>[]) {
const [columnVisibility, setColumnVisibility] = useLocalStorage({ key: 'table-hidden', defaultValue: {} });
const [columnOrder, saveColumnOrder] = useLocalStorage<string[]>({
key: 'table-order',
defaultValue: columns.map((col) => col.id as string),
});
const [columnSizing, setColumnSizing] = useLocalStorage({ key: 'table-sizes', defaultValue: {} });
// if the columns change, we update the dataset
useEffect(() => {
let shouldReplace = false;
const newColumns: string[] = [];
// iterate through columns to see if there are new ids
columns.forEach((column) => {
const columnnId = column.id as string;
if (!shouldReplace && !columnOrder.includes(columnnId)) {
shouldReplace = true;
}
newColumns.push(columnnId);
});
if (shouldReplace) {
saveColumnOrder(newColumns);
}
}, [columnOrder, columns, saveColumnOrder]);
const resetColumnOrder = useCallback(() => {
saveColumnOrder(columns.map((col) => col.id as string));
}, [columns, saveColumnOrder]);
return {
columnVisibility,
columnOrder,
columnSizing,
resetColumnOrder,
setColumnVisibility,
saveColumnOrder,
setColumnSizing,
};
}
@@ -65,12 +65,15 @@
font-size: 1.5rem; font-size: 1.5rem;
letter-spacing: 0.5px; letter-spacing: 0.5px;
color: $ui-white; color: $ui-white;
@include ellipsis-text;
} }
.secondaryField { .secondaryField {
grid-area: secondary; grid-area: secondary;
font-size: 1.25rem; font-size: 1.25rem;
letter-spacing: 0.5px; letter-spacing: 0.5px;
@include ellipsis-text;
} }
.schedule { .schedule {
@@ -96,10 +99,10 @@
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
.field { .field {
font-weight: 600; font-weight: 600;
padding: 0 0.25rem; padding-inline: 0.25rem;
margin-right: 0.25rem;
} }
.noColour { .noColour {
@@ -110,6 +113,8 @@
} }
.value { .value {
padding-inline: 0.25rem;
background-color: $gray-1250;
color: var(--operator-highlight-override, $ui-white); color: var(--operator-highlight-override, $ui-white);
margin-right: 1rem; margin-right: 1rem;
} }
@@ -6,14 +6,14 @@ import { EditorUpdateFields } from '../EventEditor';
import style from '../EventEditor.module.scss'; import style from '../EventEditor.module.scss';
interface CountedTextInputProps extends InputProps { interface EventTextInputProps extends InputProps {
field: EditorUpdateFields; field: EditorUpdateFields;
label: string; label: string;
initialValue: string; initialValue: string;
submitHandler: (field: EditorUpdateFields, value: string) => void; submitHandler: (field: EditorUpdateFields, value: string) => void;
} }
export default function EventTextInput(props: CountedTextInputProps) { export default function EventTextInput(props: EventTextInputProps) {
const { field, label, initialValue, submitHandler, maxLength } = props; const { field, label, initialValue, submitHandler, maxLength } = props;
const ref = useRef<HTMLInputElement | null>(null); const ref = useRef<HTMLInputElement | null>(null);
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]); const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
@@ -34,7 +34,7 @@ export default function EventTextInput(props: CountedTextInputProps) {
variant='ontime-filled' variant='ontime-filled'
data-testid='input-textfield' data-testid='input-textfield'
value={value} value={value}
maxLength={maxLength || 50} maxLength={maxLength || 100}
onChange={onChange} onChange={onChange}
onBlur={onBlur} onBlur={onBlur}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
@@ -25,6 +25,8 @@ import { getPropertyValue } from '../common/viewUtils';
import './Backstage.scss'; import './Backstage.scss';
export const MotionTitleCard = motion(TitleCard);
interface BackstageProps { interface BackstageProps {
customFields: CustomFields; customFields: CustomFields;
isMirrored: boolean; isMirrored: boolean;
@@ -124,7 +126,7 @@ export default function Backstage(props: BackstageProps) {
animate='visible' animate='visible'
exit='exit' exit='exit'
> >
<TitleCard label='now' title={eventNow.title} secondary={secondaryTextNow} /> <TitleCard title={eventNow.title} secondary={secondaryTextNow} />
<div className='timer-group'> <div className='timer-group'>
<div className='aux-timers'> <div className='aux-timers'>
<div className='aux-timers__label'>{getLocalizedString('common.started_at')}</div> <div className='aux-timers__label'>{getLocalizedString('common.started_at')}</div>
@@ -151,16 +153,17 @@ export default function Backstage(props: BackstageProps) {
<AnimatePresence> <AnimatePresence>
{eventNext && ( {eventNext && (
<motion.div <MotionTitleCard
className='event next' className='event next'
key='next' key='next'
variants={titleVariants} variants={titleVariants}
initial='hidden' initial='hidden'
animate='visible' animate='visible'
exit='exit' exit='exit'
> label='next'
<TitleCard label='next' title={eventNext.title} secondary={secondaryTextNext} /> title={eventNext.title}
</motion.div> secondary={secondaryTextNext}
/>
)} )}
</AnimatePresence> </AnimatePresence>
</div> </div>
@@ -1,7 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { OntimeEvent, OntimeRundownEntry, Playback, Settings, SupportedEvent, ViewSettings } from 'ontime-types'; import { OntimeEvent, OntimeRundownEntry, Playback, Settings, SupportedEvent, ViewSettings } from 'ontime-types';
import { millisToString, removeLeadingZero } from 'ontime-utils';
import { overrideStylesURL } from '../../../common/api/constants'; import { overrideStylesURL } from '../../../common/api/constants';
import { getCountdownOptions } from '../../../common/components/view-params-editor/constants'; import { getCountdownOptions } from '../../../common/components/view-params-editor/constants';
@@ -12,6 +11,7 @@ import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
import { formatTime, getDefaultFormat } from '../../../common/utils/time'; import { formatTime, getDefaultFormat } from '../../../common/utils/time';
import { useTranslation } from '../../../translation/TranslationProvider'; import { useTranslation } from '../../../translation/TranslationProvider';
import SuperscriptTime from '../common/superscript-time/SuperscriptTime'; import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
import { getFormattedTimer, isStringBoolean } from '../common/viewUtils';
import { fetchTimerData, TimerMessage } from './countdown.helpers'; import { fetchTimerData, TimerMessage } from './countdown.helpers';
import CountdownSelect from './CountdownSelect'; import CountdownSelect from './CountdownSelect';
@@ -84,24 +84,17 @@ export default function Countdown(props: CountdownProps) {
const standby = time.playback !== Playback.Play && time.playback !== Playback.Roll && selectedId === follow?.id; const standby = time.playback !== Playback.Play && time.playback !== Playback.Roll && selectedId === follow?.id;
const finished = time.playback === Playback.Play && (time.current ?? 0) < 0 && time.startedAt; const finished = time.playback === Playback.Play && (time.current ?? 0) < 0 && time.startedAt;
const isRunningFinished = finished && runningMessage === TimerMessage.running; const isRunningFinished = finished && runningMessage === TimerMessage.running;
const isSelected = runningMessage === TimerMessage.running;
const delayedTimerStyles = delay > 0 ? 'aux-timers__value--delayed' : ''; const delayedTimerStyles = delay > 0 ? 'aux-timers__value--delayed' : '';
const clock = formatTime(time.clock); const clock = formatTime(time.clock);
const startTime = follow === null ? '...' : formatTime(follow.timeStart + delay); const startTime = follow === null ? '...' : formatTime(follow.timeStart + delay);
const endTime = follow === null ? '...' : formatTime(follow.timeEnd + delay); const endTime = follow === null ? '...' : formatTime(follow.timeEnd + delay);
const formatTimer = (): string => { const hideSeconds = searchParams.get('hideTimerSeconds');
if (runningMessage === TimerMessage.ended) { const formattedTimer = getFormattedTimer(runningTimer, time.timerType, getLocalizedString('common.minutes'), {
return formatTime(runningTimer, { format12: 'hh:mm a', format24: 'HH:mm' }); removeSeconds: isStringBoolean(hideSeconds),
} removeLeadingZero: false,
let formattedTime = millisToString(isSelected ? runningTimer : runningTimer + delay); });
if (isSelected || runningMessage === TimerMessage.waiting) {
formattedTime = removeLeadingZero(formattedTime);
}
return formattedTime;
};
const formattedTimer = formatTimer();
const defaultFormat = getDefaultFormat(settings?.timeFormat); const defaultFormat = getDefaultFormat(settings?.timeFormat);
const timeOption = getCountdownOptions(defaultFormat); const timeOption = getCountdownOptions(defaultFormat);
@@ -72,6 +72,8 @@
font-family: var(--lowerThird-font-family-override), Lato, Arial, sans-serif; font-family: var(--lowerThird-font-family-override), Lato, Arial, sans-serif;
text-align: var(--lowerThird-text-align-override, left); text-align: var(--lowerThird-text-align-override, left);
white-space: nowrap; white-space: nowrap;
@include ellipsis-text();
&::after { &::after {
content: '\200b'; content: '\200b';
} }
@@ -21,6 +21,8 @@ import { getPropertyValue } from '../common/viewUtils';
import './Public.scss'; import './Public.scss';
export const MotionTitleCard = motion(TitleCard);
interface BackstageProps { interface BackstageProps {
customFields: CustomFields; customFields: CustomFields;
isMirrored: boolean; isMirrored: boolean;
@@ -83,31 +85,33 @@ export default function Public(props: BackstageProps) {
<div className='now-container'> <div className='now-container'>
<AnimatePresence> <AnimatePresence>
{publicEventNow && ( {publicEventNow && (
<motion.div <MotionTitleCard
className='event now' className='event now'
key='now' key='now'
variants={titleVariants} variants={titleVariants}
initial='hidden' initial='hidden'
animate='visible' animate='visible'
exit='exit' exit='exit'
> label='now'
<TitleCard label='now' title={publicEventNow.title} secondary={secondaryTextNow} /> title={publicEventNow.title}
</motion.div> secondary={secondaryTextNow}
/>
)} )}
</AnimatePresence> </AnimatePresence>
<AnimatePresence> <AnimatePresence>
{publicEventNext && ( {publicEventNext && (
<motion.div <MotionTitleCard
className='event next' className='event next'
key='next' key='next'
variants={titleVariants} variants={titleVariants}
initial='hidden' initial='hidden'
animate='visible' animate='visible'
exit='exit' exit='exit'
> label='next'
<TitleCard label='next' title={publicEventNext.title} secondary={secondaryTextNext} /> title={publicEventNext.title}
</motion.div> secondary={secondaryTextNext}
/>
)} )}
</AnimatePresence> </AnimatePresence>
</div> </div>
@@ -168,7 +168,6 @@ $orange-active: #f60;
display: flex; display: flex;
align-items: start; align-items: start;
gap: 0.5em; gap: 0.5em;
white-space: nowrap;
&--now { &--now {
color: var(--studio-active-label, $red-active); color: var(--studio-active-label, $red-active);
@@ -36,7 +36,7 @@ export default function StudioClock(props: StudioClockProps) {
// TODO: can we prevent the Flash of Unstyled Content on the 7segment fonts? // TODO: can we prevent the Flash of Unstyled Content on the 7segment fonts?
// deferring rendering seems to affect styling (font and useFitText) // deferring rendering seems to affect styling (font and useFitText)
useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL); useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const { fontSize: titleFontSize, ref: titleRef } = useFitText({ maxFontSize: 500 }); const { fontSize: titleFontSize, ref: titleRef } = useFitText({ minFontSize: 150, maxFontSize: 500 });
const activeIndicators = [...Array(12).keys()]; const activeIndicators = [...Array(12).keys()];
const secondsIndicators = [...Array(60).keys()]; const secondsIndicators = [...Array(60).keys()];
@@ -15,7 +15,7 @@
padding: min(2vh, 16px) clamp(16px, 10vw, 64px); padding: min(2vh, 16px) clamp(16px, 10vw, 64px);
display: grid; display: grid;
grid-template-columns: auto; grid-template-columns: 100%;
grid-template-rows: auto 1fr auto auto auto; grid-template-rows: auto 1fr auto auto auto;
grid-template-areas: grid-template-areas:
'clock' 'clock'
@@ -43,6 +43,8 @@ const titleVariants = {
}, },
}; };
export const MotionTitleCard = motion(TitleCard);
interface TimerProps { interface TimerProps {
customFields: CustomFields; customFields: CustomFields;
eventNext: OntimeEvent | null; eventNext: OntimeEvent | null;
@@ -211,31 +213,33 @@ export default function Timer(props: TimerProps) {
<> <>
<AnimatePresence> <AnimatePresence>
{eventNow?.title && ( {eventNow?.title && (
<motion.div <MotionTitleCard
className='event now' className='event now'
key='now' key='now'
variants={titleVariants} variants={titleVariants}
initial='hidden' initial='hidden'
animate='visible' animate='visible'
exit='exit' exit='exit'
> label='now'
<TitleCard label='now' title={mainFieldNow} secondary={secondaryTextNow} /> title={mainFieldNow}
</motion.div> secondary={secondaryTextNow}
/>
)} )}
</AnimatePresence> </AnimatePresence>
<AnimatePresence> <AnimatePresence>
{eventNext?.title && ( {eventNext?.title && (
<motion.div <MotionTitleCard
className='event next'
key='next' key='next'
variants={titleVariants} variants={titleVariants}
initial='hidden' initial='hidden'
animate='visible' animate='visible'
exit='exit' exit='exit'
> label='next'
<TitleCard label='next' title={mainFieldNext} secondary={secondaryTextNext} /> title={mainFieldNext}
</motion.div> secondary={secondaryTextNext}
className='event next'
/>
)} )}
</AnimatePresence> </AnimatePresence>
</> </>
+7 -1
View File
@@ -1,4 +1,4 @@
@use "ontimeStyles" as *; @use 'ontimeStyles' as *;
//////////////////////////////////// general app elements //////////////////////////////////// general app elements
@@ -15,3 +15,9 @@
color: $ontime-color; color: $ontime-color;
} }
} }
@mixin ellipsis-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@@ -8,6 +8,7 @@ import { langEs } from './languages/es';
import { langFr } from './languages/fr'; import { langFr } from './languages/fr';
import { langIt } from './languages/it'; import { langIt } from './languages/it';
import { langNo } from './languages/no'; import { langNo } from './languages/no';
import { langPl } from './languages/pl';
import { langPt } from './languages/pt'; import { langPt } from './languages/pt';
import { langSv } from './languages/sv'; import { langSv } from './languages/sv';
@@ -20,6 +21,7 @@ const translationsList = {
no: langNo, no: langNo,
pt: langPt, pt: langPt,
sv: langSv, sv: langSv,
pl: langPl,
}; };
interface TranslationContextValue { interface TranslationContextValue {
@@ -0,0 +1,21 @@
import { TranslationObject } from './en';
export const langPl: TranslationObject = {
'common.end_time': 'Czas zakończenia',
'common.expected_finish': 'Zakładany czas zakończenia',
'common.minutes': 'min',
'common.now': 'Teraz',
'common.next': 'Następnie',
'common.public_message': 'Wiadomość publiczna',
'common.start_time': 'Czas rozpoczęcia',
'common.stage_timer': 'Timer Scena',
'common.started_at': 'Rozpoczęte o',
'common.time_now': 'Aktualny czas',
'countdown.ended': 'Zakończone o',
'countdown.running': 'Trwa',
'countdown.select_event': 'Wybierz event który chcesz śledzić',
'countdown.to_start': 'Do rozpoczęcia',
'countdown.waiting': 'Oczekiwanie na start',
'countdown.overtime': 'ponad czasem',
};
+1
View File
@@ -61,6 +61,7 @@ export default defineConfig({
additionalData: ` additionalData: `
@use './src/theme/ontimeColours' as *; @use './src/theme/ontimeColours' as *;
@use './src/theme/ontimeStyles' as *; @use './src/theme/ontimeStyles' as *;
@use './src/theme/mixins' as *;
`, `,
}, },
}, },
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime", "name": "ontime",
"version": "3.3.0", "version": "3.3.3",
"author": "Carlos Valente", "author": "Carlos Valente",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime", "repository": "https://github.com/cpvalente/ontime",
+3 -3
View File
@@ -2,7 +2,7 @@
"name": "ontime-server", "name": "ontime-server",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"version": "3.3.0", "version": "3.3.3",
"exports": "./src/index.js", "exports": "./src/index.js",
"dependencies": { "dependencies": {
"@googleapis/sheets": "^5.0.5", "@googleapis/sheets": "^5.0.5",
@@ -20,8 +20,7 @@
"node-xlsx": "^0.23.0", "node-xlsx": "^0.23.0",
"ontime-utils": "workspace:*", "ontime-utils": "workspace:*",
"sanitize-filename": "^1.6.3", "sanitize-filename": "^1.6.3",
"steno": "^3.1.0", "steno": "^4.0.2",
"ts-essentials": "^9.4.1",
"ws": "^8.13.0" "ws": "^8.13.0"
}, },
"devDependencies": { "devDependencies": {
@@ -44,6 +43,7 @@
"shx": "^0.3.4", "shx": "^0.3.4",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.4.3", "typescript": "^5.4.3",
"ts-essentials": "^9.4.1",
"vitest": "^1.6.0" "vitest": "^1.6.0"
}, },
"scripts": { "scripts": {
+2 -1
View File
@@ -18,6 +18,7 @@ import {
validatePatchProject, validatePatchProject,
validateFilenameBody, validateFilenameBody,
validateFilenameParam, validateFilenameParam,
validateNewFilenameBody,
} from './db.validation.js'; } from './db.validation.js';
export const router = express.Router(); export const router = express.Router();
@@ -31,7 +32,7 @@ router.post('/new', validateFilenameBody, validateNewProject, createProjectFile)
router.get('/all', listProjects); router.get('/all', listProjects);
router.post('/load', validateFilenameBody, loadProject); router.post('/load', validateFilenameBody, loadProject);
router.post('/:filename/duplicate', validateFilenameParam, validateFilenameBody, duplicateProjectFile); router.post('/:filename/duplicate', validateFilenameParam, validateNewFilenameBody, duplicateProjectFile);
router.put('/:filename/rename', validateFilenameParam, validateFilenameBody, renameProjectFile); router.put('/:filename/rename', validateFilenameParam, validateFilenameBody, renameProjectFile);
router.delete('/:filename', validateFilenameParam, deleteProjectFile); router.delete('/:filename', validateFilenameParam, deleteProjectFile);
@@ -41,6 +41,30 @@ export const validatePatchProject = [
}, },
]; ];
/**
* @description Validates request with newFilename in the body.
*/
export const validateNewFilenameBody = [
body('newFilename')
.exists()
.isString()
.trim()
.customSanitizer((input: string) => sanitize(input))
.withMessage('Failed to sanitize the filename')
.notEmpty()
.withMessage('Filename was empty or contained only invalid characters')
.customSanitizer((input: string) => ensureJsonExtension(input)),
(req: Request, res: Response, next: NextFunction) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() });
}
next();
},
];
/** /**
* @description Validates request with filename in the body. * @description Validates request with filename in the body.
*/ */
+6 -3
View File
@@ -17,7 +17,6 @@ import {
resolveExternalsDirectory, resolveExternalsDirectory,
resolveStylesDirectory, resolveStylesDirectory,
resolvedPath, resolvedPath,
clearUploadfolder,
} from './setup/index.js'; } from './setup/index.js';
import { ONTIME_VERSION } from './ONTIME_VERSION.js'; import { ONTIME_VERSION } from './ONTIME_VERSION.js';
import { consoleSuccess, consoleHighlight } from './utils/console.js'; import { consoleSuccess, consoleHighlight } from './utils/console.js';
@@ -44,6 +43,9 @@ import { messageService } from './services/message-service/MessageService.js';
import { populateDemo } from './setup/loadDemo.js'; import { populateDemo } from './setup/loadDemo.js';
import { getState } from './stores/runtimeState.js'; import { getState } from './stores/runtimeState.js';
import { initRundown } from './services/rundown-service/RundownService.js'; import { initRundown } from './services/rundown-service/RundownService.js';
// Utilities
import { clearUploadfolder } from './utils/upload.js';
import { generateCrashReport } from './utils/generateCrashReport.js'; import { generateCrashReport } from './utils/generateCrashReport.js';
import { getNetworkInterfaces } from './utils/networkInterfaces.js'; import { getNetworkInterfaces } from './utils/networkInterfaces.js';
@@ -208,8 +210,9 @@ export const startServer = async (
expressServer.listen(serverPort, '0.0.0.0', () => { expressServer.listen(serverPort, '0.0.0.0', () => {
const nif = getNetworkInterfaces(); const nif = getNetworkInterfaces();
consoleSuccess(`Local: http://localhost:${serverPort}/editor`); consoleSuccess(`Local: http://localhost:${serverPort}/editor`);
for (const key of Object.keys(nif)) { for (const key in nif) {
consoleSuccess(`Network: http://${nif[key].address}:${serverPort}/editor`); const address = nif[key].address;
consoleSuccess(`Network: http://${address}:${serverPort}/editor`);
} }
}); });
+4 -4
View File
@@ -4,11 +4,11 @@ import { generateId, millisToString } from 'ontime-utils';
import { clock } from '../services/Clock.js'; import { clock } from '../services/Clock.js';
import { isProduction } from '../setup/index.js'; import { isProduction } from '../setup/index.js';
import { socket } from '../adapters/WebsocketAdapter.js'; import { socket } from '../adapters/WebsocketAdapter.js';
import { consoleSubdued, consoleRed } from '../utils/console.js'; import { consoleSubdued, consoleError } from '../utils/console.js';
class Logger { class Logger {
private queue: Log[]; private queue: Log[];
private escalateErrorFn: (error: string) => void | null; private escalateErrorFn: ((error: string) => void) | null;
private canLog = false; private canLog = false;
constructor() { constructor() {
@@ -20,7 +20,7 @@ class Logger {
/** /**
* Enabling setup logger after init * Enabling setup logger after init
*/ */
init(escalateErrorFn: (error: string) => void) { init(escalateErrorFn?: (error: string) => void) {
// flush logs from queue // flush logs from queue
this.queue.forEach((log) => { this.queue.forEach((log) => {
this._push(log); this._push(log);
@@ -47,7 +47,7 @@ class Logger {
private _push(log: Log) { private _push(log: Log) {
if (this.canLog || log.level === LogLevel.Severe) { if (this.canLog || log.level === LogLevel.Severe) {
if (log.level === LogLevel.Severe) { if (log.level === LogLevel.Severe) {
consoleRed(`[${log.level}] \t ${log.origin} \t ${log.text}`); consoleError(`[${log.level}] \t ${log.origin} \t ${log.text}`);
} else { } else {
consoleSubdued(`[${log.level}] \t ${log.origin} \t ${log.text}`); consoleSubdued(`[${log.level}] \t ${log.origin} \t ${log.text}`);
} }
@@ -1,4 +1,4 @@
import { DatabaseModel, OntimeRundown, Settings, URLPreset } from 'ontime-types'; import { DatabaseModel, OntimeRundown, Settings, URLPreset, ViewSettings } from 'ontime-types';
import { safeMerge } from '../DataProvider.utils.js'; import { safeMerge } from '../DataProvider.utils.js';
describe('safeMerge', () => { describe('safeMerge', () => {
@@ -49,7 +49,7 @@ describe('safeMerge', () => {
} as DatabaseModel; } as DatabaseModel;
it('returns existing data if new data is not provided', () => { it('returns existing data if new data is not provided', () => {
const mergedData = safeMerge(existing, undefined); const mergedData = safeMerge(existing, {});
expect(mergedData).toEqual(existing); expect(mergedData).toEqual(existing);
}); });
@@ -136,6 +136,7 @@ describe('safeMerge', () => {
rundown: [], rundown: [],
project: { project: {
title: '', title: '',
description: '',
publicUrl: '', publicUrl: '',
publicInfo: '', publicInfo: '',
backstageUrl: '', backstageUrl: '',
@@ -153,8 +154,9 @@ describe('safeMerge', () => {
viewSettings: { viewSettings: {
overrideStyles: false, overrideStyles: false,
endMessage: '', endMessage: '',
}, } as ViewSettings,
urlPresets: [], urlPresets: [],
customFields: {},
osc: { osc: {
portIn: 8888, portIn: 8888,
portOut: 9999, portOut: 9999,
@@ -163,6 +165,10 @@ describe('safeMerge', () => {
enabledOut: false, enabledOut: false,
subscriptions: [], subscriptions: [],
}, },
http: {
enabledOut: false,
subscriptions: [],
},
} as DatabaseModel; } as DatabaseModel;
const newData = { const newData = {
+2 -2
View File
@@ -1,5 +1,5 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import { consoleHighlight, consoleRed } from './utils/console.js'; import { consoleHighlight, consoleError } from './utils/console.js';
import { initAssets, startIntegrations, startServer } from './app.js'; import { initAssets, startIntegrations, startServer } from './app.js';
async function startOntime() { async function startOntime() {
@@ -16,7 +16,7 @@ async function startOntime() {
consoleHighlight('Request: Start integrations...'); consoleHighlight('Request: Start integrations...');
await startIntegrations(); await startIntegrations();
} catch (error) { } catch (error) {
consoleRed(`Request failed: ${error}`); consoleError(`Request failed: ${error}`);
} }
} }
@@ -14,21 +14,24 @@ interface Config {
class AppStateService { class AppStateService {
private config: Low<Config>; private config: Low<Config>;
private pathToFile: string; private pathToFile: string;
private didInit = false;
constructor(appStatePath: string) { constructor(appStatePath: string) {
this.pathToFile = appStatePath; this.pathToFile = appStatePath;
const adapter = new JSONFile<Config>(this.pathToFile); const adapter = new JSONFile<Config>(this.pathToFile);
this.config = new Low<Config>(adapter, null); this.config = new Low<Config>(adapter, null);
this.init();
} }
private async init() { private async init() {
await this.config.read(); await this.config.read();
await this.config.write(); await this.config.write();
this.didInit = true;
} }
async get(): Promise<Config> { async get(): Promise<Config> {
if (!this.didInit) {
await this.init();
}
await this.config.read(); await this.config.read();
return this.config.data; return this.config.data;
} }
@@ -36,6 +39,10 @@ class AppStateService {
async updateDatabaseConfig(filename: string): Promise<void> { async updateDatabaseConfig(filename: string): Promise<void> {
if (isTest) return; if (isTest) return;
if (!this.didInit) {
await this.init();
}
this.config.data.lastLoadedProject = filename; this.config.data.lastLoadedProject = filename;
await this.config.write(); await this.config.write();
} }
@@ -170,6 +170,19 @@ describe('generate()', () => {
expect(initResult.totalDuration).toBe(500 - 100); expect(initResult.totalDuration).toBe(500 - 100);
}); });
it('calculates total duration with 0 duration events without causing a next day', () => {
const testRundown: OntimeRundown = [
{ type: SupportedEvent.Event, id: '1', timeStart: 100, timeEnd: 100, duration: 0 } as OntimeEvent,
{ type: SupportedEvent.Event, id: '2', timeStart: 100, timeEnd: 300 } as OntimeEvent,
{ type: SupportedEvent.Event, id: 'skipped', skip: true, timeStart: 300, timeEnd: 400 } as OntimeEvent,
{ type: SupportedEvent.Event, id: '3', timeStart: 400, timeEnd: 500 } as OntimeEvent,
];
const initResult = generate(testRundown);
expect(initResult.order.length).toBe(4);
expect(initResult.totalDuration).toBe(500 - 100);
});
it('calculates total duration across days with gap', () => { it('calculates total duration across days with gap', () => {
const testRundown: OntimeRundown = [ const testRundown: OntimeRundown = [
{ {
@@ -361,7 +374,7 @@ describe('remove() mutation', () => {
]; ];
const { newRundown } = remove({ eventIds: ['1', '2', '3'], persistedRundown: testRundown }); const { newRundown } = remove({ eventIds: ['1', '2', '3'], persistedRundown: testRundown });
expect(newRundown.length).toBe(3); expect(newRundown.length).toBe(3);
expect(newRundown.at(0).id).toBe('4'); expect(newRundown.at(0)?.id).toBe('4');
}); });
}); });
@@ -87,6 +87,7 @@ export function generate(
let daySpan = 0; let daySpan = 0;
let previousStart: MaybeNumber = null; let previousStart: MaybeNumber = null;
let previousEnd: MaybeNumber = null; let previousEnd: MaybeNumber = null;
let previousDuration: MaybeNumber = null;
for (let i = 0; i < initialRundown.length; i++) { for (let i = 0; i < initialRundown.length; i++) {
const currentEvent = initialRundown[i]; const currentEvent = initialRundown[i];
@@ -111,7 +112,8 @@ export function generate(
lastEnd = updatedEvent.timeEnd; lastEnd = updatedEvent.timeEnd;
// check if we go over midnight, account for eventual gaps // check if we go over midnight, account for eventual gaps
const gapOverMidnight = previousStart !== null && checkIsNextDay(previousStart, updatedEvent.timeStart); const gapOverMidnight =
previousStart !== null && checkIsNextDay(previousStart, updatedEvent.timeStart, previousDuration);
const durationOverMidnight = updatedEvent.timeStart > updatedEvent.timeEnd; const durationOverMidnight = updatedEvent.timeStart > updatedEvent.timeEnd;
if (gapOverMidnight || durationOverMidnight) { if (gapOverMidnight || durationOverMidnight) {
daySpan++; daySpan++;
@@ -134,6 +136,7 @@ export function generate(
updatedEvent.delay = accumulatedDelay; updatedEvent.delay = accumulatedDelay;
previousStart = updatedEvent.timeStart; previousStart = updatedEvent.timeStart;
previousEnd = updatedEvent.timeEnd; previousEnd = updatedEvent.timeEnd;
previousDuration = updatedEvent.duration;
} }
order.push(updatedEvent.id); order.push(updatedEvent.id);
@@ -85,7 +85,7 @@ class RuntimeService {
integrationService.dispatch(TimerLifeCycle.onUpdate); integrationService.dispatch(TimerLifeCycle.onUpdate);
}); });
this.lastIntegrationTimerValue = newState.timer.current; this.lastIntegrationTimerValue = newState.timer.current ?? -1;
} }
const shouldUpdateClock = getShouldClockUpdate(this.lastIntegrationClockUpdate, newState.clock); const shouldUpdateClock = getShouldClockUpdate(this.lastIntegrationClockUpdate, newState.clock);
@@ -5,6 +5,7 @@
*/ */
import { AuthenticationStatus, CustomFields, LogOrigin, MaybeString, OntimeRundown } from 'ontime-types'; import { AuthenticationStatus, CustomFields, LogOrigin, MaybeString, OntimeRundown } from 'ontime-types';
import { ImportMap, getErrorMessage } from 'ontime-utils';
import { sheets, sheets_v4 } from '@googleapis/sheets'; import { sheets, sheets_v4 } from '@googleapis/sheets';
import { Credentials, OAuth2Client } from 'google-auth-library'; import { Credentials, OAuth2Client } from 'google-auth-library';
@@ -13,7 +14,6 @@ import got from 'got';
import { resolveSheetsDirectory } from '../../setup/index.js'; import { resolveSheetsDirectory } from '../../setup/index.js';
import { ensureDirectory } from '../../utils/fileManagement.js'; import { ensureDirectory } from '../../utils/fileManagement.js';
import { cellRequestFromEvent, type ClientSecret, getA1Notation, validateClientSecret } from './sheetUtils.js'; import { cellRequestFromEvent, type ClientSecret, getA1Notation, validateClientSecret } from './sheetUtils.js';
import { ImportMap } from 'ontime-utils';
import { parseExcel } from '../../utils/parser.js'; import { parseExcel } from '../../utils/parser.js';
import { logger } from '../../classes/Logger.js'; import { logger } from '../../classes/Logger.js';
import { parseCustomFields, parseRundown } from '../../utils/parserFunctions.js'; import { parseCustomFields, parseRundown } from '../../utils/parserFunctions.js';
@@ -208,7 +208,8 @@ async function verifySheet(
}); });
return { worksheetOptions: spreadsheets.data.sheets.map((i) => i.properties.title) }; return { worksheetOptions: spreadsheets.data.sheets.map((i) => i.properties.title) };
} catch (error) { } catch (error) {
throw new Error(`Failed to verify sheet: ${error.message}`); const errorMessage = getErrorMessage(error);
throw new Error(`Failed to verify sheet: ${errorMessage}`);
} }
} }
@@ -60,7 +60,7 @@ describe('cellRequestFromEvent()', () => {
timeDanger: { row: 1, col: 41 }, timeDanger: { row: 1, col: 41 },
}; };
const result = cellRequestFromEvent(event, 1, 1234, metadata); const result = cellRequestFromEvent(event, 1, 1234, metadata);
expect(result.updateCells.rows[0].values[5].userEnteredValue.stringValue).toStrictEqual(event.note); expect(result.updateCells?.rows?.at(0)?.values?.at(5)?.userEnteredValue?.stringValue).toStrictEqual(event.note);
}); });
test('number to timer', () => { test('number to timer', () => {
@@ -103,9 +103,10 @@ describe('cellRequestFromEvent()', () => {
timeWarning: { row: 1, col: 40 }, timeWarning: { row: 1, col: 40 },
timeDanger: { row: 1, col: 41 }, timeDanger: { row: 1, col: 41 },
}; };
const result = cellRequestFromEvent(event, 1, 1234, metadata).updateCells.rows[0].values[10].userEnteredValue const result = cellRequestFromEvent(event, 1, 1234, metadata);
.stringValue; expect(result.updateCells?.rows?.at(0)?.values?.at(10)?.userEnteredValue?.stringValue).toStrictEqual(
expect(result).toStrictEqual(millisToString(event.duration)); millisToString(event.duration),
);
}); });
test('boolean to TRUE', () => { test('boolean to TRUE', () => {
@@ -149,8 +150,8 @@ describe('cellRequestFromEvent()', () => {
timeDanger: { row: 1, col: 41 }, timeDanger: { row: 1, col: 41 },
}; };
const result = cellRequestFromEvent(event, 1, 1234, metadata); const result = cellRequestFromEvent(event, 1, 1234, metadata);
expect(result.updateCells.rows[0].values[11].userEnteredValue.boolValue).toStrictEqual(true); expect(result.updateCells?.rows?.at(0)?.values?.at(11)?.userEnteredValue?.boolValue).toStrictEqual(true);
expect(result.updateCells.rows[0].values[12].userEnteredValue.boolValue).toStrictEqual(false); expect(result.updateCells?.rows?.at(0)?.values?.at(12)?.userEnteredValue?.boolValue).toStrictEqual(false);
}); });
test('spacing in metadata', () => { test('spacing in metadata', () => {
@@ -180,8 +181,8 @@ describe('cellRequestFromEvent()', () => {
title: { row: 1, col: 6 }, title: { row: 1, col: 6 },
}; };
const result = cellRequestFromEvent(event, 1, 1234, metadata); const result = cellRequestFromEvent(event, 1, 1234, metadata);
expect(result.updateCells.rows[0].values[0].userEnteredValue.stringValue).toStrictEqual(event.cue); expect(result.updateCells?.rows?.at(0)?.values?.at(0)?.userEnteredValue?.stringValue).toStrictEqual(event.cue);
expect(result.updateCells.rows[0].values[6].userEnteredValue.stringValue).toStrictEqual(event.title); expect(result.updateCells?.rows?.at(0)?.values?.at(6)?.userEnteredValue?.stringValue).toStrictEqual(event.title);
}); });
test('metadata offset from zero', () => { test('metadata offset from zero', () => {
@@ -212,8 +213,8 @@ describe('cellRequestFromEvent()', () => {
user0: { row: 1, col: 16 }, user0: { row: 1, col: 16 },
}; };
const result = cellRequestFromEvent(event, 1, 1234, metadata); const result = cellRequestFromEvent(event, 1, 1234, metadata);
expect(result.updateCells.rows[0].values[0].userEnteredValue.stringValue).toStrictEqual(event.cue); expect(result.updateCells?.rows?.at(0)?.values?.at(0)?.userEnteredValue?.stringValue).toStrictEqual(event.cue);
expect(result.updateCells.rows[0].values[1].userEnteredValue.stringValue).toStrictEqual(event.title); expect(result.updateCells?.rows?.at(0)?.values?.at(1)?.userEnteredValue?.stringValue).toStrictEqual(event.title);
}); });
test('sheet setup', () => { test('sheet setup', () => {
@@ -243,10 +244,10 @@ describe('cellRequestFromEvent()', () => {
title: { row: 10, col: 6 }, title: { row: 10, col: 6 },
}; };
const result1 = cellRequestFromEvent(event, 1, 1234, metadata); const result1 = cellRequestFromEvent(event, 1, 1234, metadata);
expect(result1.updateCells.start.sheetId).toStrictEqual(1234); expect(result1.updateCells?.start?.sheetId).toStrictEqual(1234);
const result2 = cellRequestFromEvent(event, 10, 1234, metadata); const result2 = cellRequestFromEvent(event, 10, 1234, metadata);
expect(result2.updateCells.start.rowIndex).toStrictEqual(21); expect(result2.updateCells?.start?.rowIndex).toStrictEqual(21);
expect(result2.updateCells.start.columnIndex).toStrictEqual(5); expect(result2.updateCells?.start?.columnIndex).toStrictEqual(5);
expect(result2.updateCells.fields).toStrictEqual('userEnteredValue'); expect(result2.updateCells?.fields).toStrictEqual('userEnteredValue');
}); });
}); });
+11 -21
View File
@@ -1,8 +1,6 @@
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import path, { dirname, join } from 'path'; import { dirname, join } from 'path';
import fs from 'fs'; import { readFileSync, writeFileSync } from 'fs';
import { rm } from 'fs/promises';
import { config } from './config.js'; import { config } from './config.js';
import { ensureDirectory } from '../utils/fileManagement.js'; import { ensureDirectory } from '../utils/fileManagement.js';
@@ -17,18 +15,18 @@ import { ensureDirectory } from '../utils/fileManagement.js';
export function getAppDataPath(): string { export function getAppDataPath(): string {
// handle docker // handle docker
if (process.env.ONTIME_DATA) { if (process.env.ONTIME_DATA) {
return path.join(process.env.ONTIME_DATA); return join(process.env.ONTIME_DATA);
} }
switch (process.platform) { switch (process.platform) {
case 'darwin': { case 'darwin': {
return path.join(process.env.HOME!, 'Library', 'Application Support', 'Ontime'); return join(process.env.HOME!, 'Library', 'Application Support', 'Ontime');
} }
case 'win32': { case 'win32': {
return path.join(process.env.APPDATA!, 'Ontime'); return join(process.env.APPDATA!, 'Ontime');
} }
case 'linux': { case 'linux': {
return path.join(process.env.HOME!, '.Ontime'); return join(process.env.HOME!, '.Ontime');
} }
default: { default: {
throw new Error('Could not resolve public folder for platform'); throw new Error('Could not resolve public folder for platform');
@@ -56,11 +54,11 @@ if (import.meta.url) {
// path to server src folder // path to server src folder
const currentDir = dirname(__dirname); const currentDir = dirname(__dirname);
// locally we are in src/setup, in the production build, this is a single file at src // locally we are in src/setup, in the production build, this is a single file at src
export const srcDirectory = isProduction ? currentDir : path.join(currentDir, '../'); export const srcDirectory = isProduction ? currentDir : join(currentDir, '../');
// resolve path to external // resolve path to external
const productionPath = path.join(srcDirectory, 'client/'); const productionPath = join(srcDirectory, 'client/');
const devPath = path.join(srcDirectory, '../../client/build/'); const devPath = join(srcDirectory, '../../client/build/');
export const resolvedPath = (): string => { export const resolvedPath = (): string => {
if (isTest) { if (isTest) {
@@ -83,12 +81,12 @@ export const uploadsFolderPath = join(getAppDataPath(), config.uploads);
const ensureAppState = () => { const ensureAppState = () => {
ensureDirectory(getAppDataPath()); ensureDirectory(getAppDataPath());
fs.writeFileSync(appStatePath, JSON.stringify({ lastLoadedProject: 'db.json' })); writeFileSync(appStatePath, JSON.stringify({ lastLoadedProject: 'db.json' }));
}; };
const getLastLoadedProject = () => { const getLastLoadedProject = () => {
try { try {
const appState = JSON.parse(fs.readFileSync(appStatePath, 'utf8')); const appState = JSON.parse(readFileSync(appStatePath, 'utf8'));
if (!appState.lastLoadedProject) { if (!appState.lastLoadedProject) {
ensureAppState(); ensureAppState();
} }
@@ -142,11 +140,3 @@ export const resolveCrashReportDirectory = getAppDataPath();
// path to projects // path to projects
export const resolveProjectsDirectory = join(getAppDataPath(), config.projects); export const resolveProjectsDirectory = join(getAppDataPath(), config.projects);
export async function clearUploadfolder() {
try {
await rm(uploadsFolderPath, { recursive: true });
} catch (_) {
//we dont care that there was no folder
}
}
@@ -160,6 +160,10 @@ describe('mutation on runtimeState', () => {
start(); start();
newState = getState(); newState = getState();
const firstStart = newState.clock; const firstStart = newState.clock;
if (newState.runtime.offset === null) {
throw new Error('Value cannot be null at this stage');
}
expect(newState.runtime.actualStart).toBe(newState.clock); expect(newState.runtime.actualStart).toBe(newState.clock);
expect(newState.runtime.offset).toBe(event1.timeStart - newState.clock); expect(newState.runtime.offset).toBe(event1.timeStart - newState.clock);
expect(newState.runtime.expectedEnd).toBe(event2.timeEnd - newState.runtime.offset); expect(newState.runtime.expectedEnd).toBe(event2.timeEnd - newState.runtime.offset);
@@ -167,7 +171,11 @@ describe('mutation on runtimeState', () => {
// 3. Next event // 3. Next event
load(event2, [event1, event2]); load(event2, [event1, event2]);
start(); start();
newState = getState(); newState = getState();
if (newState.runtime.actualStart === null || newState.runtime.offset === null) {
throw new Error('Value cannot be null at this stage');
}
// there is a case where the calculation time overflows the millisecond which makes // there is a case where the calculation time overflows the millisecond which makes
// tests fail // tests fail
@@ -182,6 +190,10 @@ describe('mutation on runtimeState', () => {
// 4. Add time // 4. Add time
addTime(10); addTime(10);
newState = getState(); newState = getState();
if (newState.runtime.offset === null) {
throw new Error('Value cannot be null at this stage');
}
expect(newState.runtime.offset).toBe(delayBefore - 10); expect(newState.runtime.offset).toBe(delayBefore - 10);
expect(newState.runtime.expectedEnd).toBe(event2.timeEnd - newState.runtime.offset); expect(newState.runtime.expectedEnd).toBe(event2.timeEnd - newState.runtime.offset);
@@ -30,7 +30,7 @@ describe('mergeObject()', () => {
test('it handles falsy values', () => { test('it handles falsy values', () => {
const a = { const a = {
first: 'yes', first: 'yes',
second: 'yes', second: 'yes' as string | null,
third: 'yes', third: 'yes',
}; };
const b = { const b = {
+1 -1
View File
@@ -10,7 +10,7 @@ export function consoleSuccess(message: string) {
/** /**
* Utility function to log messages in red * Utility function to log messages in red
*/ */
export function consoleRed(message: string) { export function consoleError(message: string) {
console.error(inRed(message)); console.error(inRed(message));
} }
+4 -1
View File
@@ -6,6 +6,7 @@ import { get } from '../services/rundown-service/rundownCache.js';
import { getState } from '../stores/runtimeState.js'; import { getState } from '../stores/runtimeState.js';
import { resolveCrashReportDirectory } from '../setup/index.js'; import { resolveCrashReportDirectory } from '../setup/index.js';
import { ensureDirectory } from './fileManagement.js';
/** /**
* Writes a file to the crash report location * Writes a file to the crash report location
* @param fileName * @param fileName
@@ -13,10 +14,12 @@ import { resolveCrashReportDirectory } from '../setup/index.js';
*/ */
function writeToFile(fileName: string, content: object) { function writeToFile(fileName: string, content: object) {
const path = join(resolveCrashReportDirectory, fileName); const path = join(resolveCrashReportDirectory, fileName);
ensureDirectory(resolveCrashReportDirectory);
try { try {
const textContent = JSON.stringify(content, null, 2); const textContent = JSON.stringify(content, null, 2);
writeFileSync(path, textContent); writeFileSync(path, textContent);
} catch (e_rror) { } catch (_) {
/** We do not handle the error here */ /** We do not handle the error here */
} }
} }
+5 -1
View File
@@ -9,7 +9,11 @@ export function getNetworkInterfaces(): { name: string; address: string }[] {
const results: { name: string; address: string }[] = []; const results: { name: string; address: string }[] = [];
for (const name of Object.keys(nets)) { for (const name of Object.keys(nets)) {
for (const net of nets[name]) { const netObjects = nets[name];
if (!netObjects) {
continue;
}
for (const net of netObjects) {
// Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses // Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses
if (net.family === 'IPv4' && !net.internal) { if (net.family === 'IPv4' && !net.internal) {
results.push({ results.push({
+1 -1
View File
@@ -53,7 +53,7 @@ export function mergeObject<T extends object>(a: T, b: Partial<T>): T {
} }
if (typeof bValue === 'object' && bValue !== null && typeof aValue === 'object' && aValue !== null) { if (typeof bValue === 'object' && bValue !== null && typeof aValue === 'object' && aValue !== null) {
// @ts-expect-error -- library side, ignore for now // @ts-expect-error -- not sure how to type this
merged[key] = deepmerge(aValue, bValue); merged[key] = deepmerge(aValue, bValue);
} else if (bValue !== undefined) { } else if (bValue !== undefined) {
merged[key] = bValue; merged[key] = bValue;
+9
View File
@@ -1,6 +1,7 @@
import multer from 'multer'; import multer from 'multer';
import path from 'path'; import path from 'path';
import fs from 'fs'; import fs from 'fs';
import { rm } from 'fs/promises';
import { ensureDirectory } from './fileManagement.js'; import { ensureDirectory } from './fileManagement.js';
import { getAppDataPath, uploadsFolderPath } from '../setup/index.js'; import { getAppDataPath, uploadsFolderPath } from '../setup/index.js';
@@ -56,3 +57,11 @@ export const storage = multer.diskStorage({
cb(null, file.originalname); cb(null, file.originalname);
}, },
}); });
export async function clearUploadfolder() {
try {
await rm(uploadsFolderPath, { recursive: true });
} catch (_) {
//we dont care that there was no folder
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime", "name": "ontime",
"version": "3.3.0", "version": "3.3.3",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"keywords": [ "keywords": [
"ontime", "ontime",
+2 -2
View File
@@ -12,8 +12,8 @@
"cleanup": "rm -rf .turbo && rm -rf node_modules" "cleanup": "rm -rf .turbo && rm -rf node_modules"
}, },
"dependencies": { "dependencies": {
"deepmerge-ts": "^5.1.0", "deepmerge-ts": "^7.0.3",
"nanoid": "^5.0.4" "nanoid": "^5.0.7"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^v7.12.0", "@typescript-eslint/eslint-plugin": "^v7.12.0",
@@ -8,6 +8,6 @@
* 09:00 - 10:00 * 09:00 - 10:00
* 09:30 - 10:30 * 09:30 - 10:30
*/ */
export function checkIsNextDay(previousStart: number, timeStart: number): boolean { export function checkIsNextDay(previousStart: number, timeStart: number, previousDuration?: number): boolean {
return timeStart <= previousStart; return previousDuration === 0 ? false : timeStart <= previousStart;
} }
+15 -19
View File
@@ -304,11 +304,8 @@ importers:
specifier: ^1.6.3 specifier: ^1.6.3
version: 1.6.3 version: 1.6.3
steno: steno:
specifier: ^3.1.0 specifier: ^4.0.2
version: 3.2.0 version: 4.0.2
ts-essentials:
specifier: ^9.4.1
version: 9.4.1(typescript@5.4.3)
ws: ws:
specifier: ^8.13.0 specifier: ^8.13.0
version: 8.13.0 version: 8.13.0
@@ -364,6 +361,9 @@ importers:
shx: shx:
specifier: ^0.3.4 specifier: ^0.3.4
version: 0.3.4 version: 0.3.4
ts-essentials:
specifier: ^9.4.1
version: 9.4.1(typescript@5.4.3)
ts-node: ts-node:
specifier: ^10.9.1 specifier: ^10.9.1
version: 10.9.1(@types/node@18.11.18)(typescript@5.4.3) version: 10.9.1(@types/node@18.11.18)(typescript@5.4.3)
@@ -392,11 +392,11 @@ importers:
packages/utils: packages/utils:
dependencies: dependencies:
deepmerge-ts: deepmerge-ts:
specifier: ^5.1.0 specifier: ^7.0.3
version: 5.1.0 version: 7.0.3
nanoid: nanoid:
specifier: ^5.0.4 specifier: ^5.0.7
version: 5.0.4 version: 5.0.7
devDependencies: devDependencies:
'@typescript-eslint/eslint-plugin': '@typescript-eslint/eslint-plugin':
specifier: ^v7.12.0 specifier: ^v7.12.0
@@ -5106,8 +5106,8 @@ packages:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
dev: true dev: true
/deepmerge-ts@5.1.0: /deepmerge-ts@7.0.3:
resolution: {integrity: sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw==} resolution: {integrity: sha512-dxFbFO2RSIhPNBPL/j8Nvdt6/vrkW9+uGf1NLah/QxBGAVbK9fj2fGTO+HwdHpPAyFAsyT9iEn/1SI9SUvespw==}
engines: {node: '>=16.0.0'} engines: {node: '>=16.0.0'}
dev: false dev: false
@@ -7547,8 +7547,8 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/nanoid@5.0.4: /nanoid@5.0.7:
resolution: {integrity: sha512-vAjmBf13gsmhXSgBrtIclinISzFFy22WwCYoyilZlsrRXNIHSwgFQ1bEdjRwMT3aoadeIF6HMuDRlOxzfXV8ig==} resolution: {integrity: sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==}
engines: {node: ^18 || >=20} engines: {node: ^18 || >=20}
hasBin: true hasBin: true
dev: false dev: false
@@ -8796,11 +8796,6 @@ packages:
resolution: {integrity: sha512-aFZ19IgVmhdB2uX599ve2kE6BIE3YMnQ6Gp6BURhW/oIzpXGKr878TQfAQZn1+i0Flcc/UKUy1gOlcfaUBCryg==} resolution: {integrity: sha512-aFZ19IgVmhdB2uX599ve2kE6BIE3YMnQ6Gp6BURhW/oIzpXGKr878TQfAQZn1+i0Flcc/UKUy1gOlcfaUBCryg==}
dev: true dev: true
/steno@3.2.0:
resolution: {integrity: sha512-zPKkv+LqoYffxrtD0GIVA08DvF6v1dW02qpP5XnERoobq9g3MKcTSBTi08gbGNFMNRo3TQV/6kBw811T1LUhKg==}
engines: {node: '>=16'}
dev: false
/steno@4.0.2: /steno@4.0.2:
resolution: {integrity: sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==} resolution: {integrity: sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -9108,7 +9103,7 @@ packages:
optional: true optional: true
dependencies: dependencies:
typescript: 5.4.3 typescript: 5.4.3
dev: false dev: true
/ts-node@10.9.1(@types/node@18.11.18)(typescript@5.4.3): /ts-node@10.9.1(@types/node@18.11.18)(typescript@5.4.3):
resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==}
@@ -9305,6 +9300,7 @@ packages:
resolution: {integrity: sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==} resolution: {integrity: sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==}
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
dev: true
/ufo@1.3.2: /ufo@1.3.2:
resolution: {integrity: sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==} resolution: {integrity: sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==}