mirror of
https://github.com/cpvalente/ontime.git
synced 2026-07-26 10:38:55 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 37786c3528 | |||
| c6d36d2497 | |||
| 2394eced93 | |||
| 13aeeb278d | |||
| af5d6e1aee | |||
| bc036bdae7 | |||
| 266a84e5d3 | |||
| 92c3148e49 | |||
| efd05bcc34 | |||
| 3c7e061e51 | |||
| 07074260a4 | |||
| c57b0423af | |||
| 52145d5e1e | |||
| b2668e61a5 | |||
| 7da3480dfe | |||
| e8cfdbe3b9 | |||
| fd052355c3 | |||
| 00f04fb78c | |||
| 6f80de58ca | |||
| 5977266332 |
@@ -128,57 +128,42 @@ Found a bug? [Open an issue](https://github.com/cpvalente/ontime/issues/new).
|
||||
|
||||
When installing the app you would see warning screens from the Operating System like:
|
||||
|
||||
in Windows
|
||||
|
||||
`Microsoft Defender SmartScreen prevented an unrecognised app from starting. Running this app might put your PC at risk.`
|
||||
|
||||
or
|
||||
|
||||
`Ontime can't be opened because it is from an unidentified developer`
|
||||
|
||||
or in Linux
|
||||
|
||||
`Could Not Display "ontime-linux.AppImage`
|
||||
|
||||
We currently only sign MacOS releases. \
|
||||
Purchasing the certificates for both Mac and Windows would mean a recurrent expense which we are not able to cover.
|
||||
|
||||
You can circumvent this by allowing the execution of the app manually.
|
||||
|
||||
- In Windows: click more and select "Run Anyway"
|
||||
- in macOS: the solution in macOS is different across versions, please refer to
|
||||
the [apple documentation](https://support.apple.com/en-gb/guide/mac-help/mh40616/mac)
|
||||
- In Linux: right-click the AppImage file -> Properties -> Permissions -> select Allow Executing
|
||||
File as a Program
|
||||
- In Windows: click `more` -> `Run Anyway`
|
||||
- In Linux: right-click the AppImage file: `Properties` -> `Permissions` -> `Allow Executing File as a Program`
|
||||
|
||||
Long story short: Ontime app is unsigned. \
|
||||
Purchasing the certificates for both Mac and Windows would mean a recurrent expense and is not a priority. \
|
||||
This is unlikely to change in future. \
|
||||
If you have tips on how to improve this or would like to sponsor the code signing,
|
||||
please [open an issue](https://github.com/cpvalente/ontime/issues/new)
|
||||
|
||||
#### Safari
|
||||
|
||||
There are known issues with Safari versions lower than 13:
|
||||
|
||||
- Spacing and text styles might have small inconsistencies
|
||||
- Table view does not work
|
||||
|
||||
There is no plan for any further work on this.
|
||||
|
||||
## Contributing
|
||||
|
||||
Looking to contribute? All types of help are appreciated, from coding to testing and feature specification.
|
||||
|
||||
If you are a developer and would like to contribute with some code, please open an issue to discuss before opening a
|
||||
Pull Request.
|
||||
If you are a developer and would like to contribute with code, please open an issue to discuss before opening a Pull Request.
|
||||
|
||||
Information about the project setup can be found in the [development documentation](./DEVELOPMENT.md)
|
||||
|
||||
## Help
|
||||
## Links
|
||||
|
||||
Help is underway! ... and can be found [here](https://docs.getontime.no)
|
||||
See the [Ontime website](https://getontime.no) here and the link to the [documentation](https://docs.getontime.no)
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the terms of the GNU GPL v3
|
||||
|
||||
## SPONSOR
|
||||
## Sponsor
|
||||
|
||||
You can help the development of this project or say thank you with a one time donation. \
|
||||
See [the terms of donations](https://github.com/cpvalente/ontime/blob/master/.github/FUNDING.md).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime-ui",
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -11,15 +11,15 @@ const dbPath = `${apiEntryUrl}/db`;
|
||||
/**
|
||||
* HTTP request to the current DB
|
||||
*/
|
||||
async function getDb(): Promise<AxiosResponse<DatabaseModel>> {
|
||||
return axios.get(`${dbPath}/download`);
|
||||
async function getDb(fileName?: string): Promise<AxiosResponse<DatabaseModel>> {
|
||||
return axios.post(`${dbPath}/download/`, { fileName });
|
||||
}
|
||||
|
||||
/**
|
||||
* Request download of the current project file
|
||||
* @param fileName
|
||||
*/
|
||||
export async function downloadProject(fileName: string = 'ontime-project') {
|
||||
export async function downloadProject(fileName: string) {
|
||||
try {
|
||||
const { data, name } = await fileDownload(fileName);
|
||||
|
||||
@@ -152,7 +152,7 @@ export async function getInfo(): Promise<GetInfo> {
|
||||
* @returns
|
||||
*/
|
||||
async function fileDownload(fileName: string): Promise<{ data: DatabaseModel; name: string }> {
|
||||
const response = await getDb();
|
||||
const response = await getDb(fileName);
|
||||
|
||||
const headerLine = response.headers['Content-Disposition'];
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND } from 'ontime-utils';
|
||||
|
||||
import { forgivingStringToMillis, millisToDelayString } from '../dateConfig';
|
||||
|
||||
describe('test forgivingStringToMillis()', () => {
|
||||
@@ -6,22 +8,22 @@ describe('test forgivingStringToMillis()', () => {
|
||||
{ value: '', expect: 0 },
|
||||
{ value: '0', expect: 0 },
|
||||
{ value: '-0', expect: 0 },
|
||||
{ value: '1', expect: 60 * 1000 },
|
||||
{ value: '-1', expect: 60 * 1000 },
|
||||
{ value: '1', expect: MILLIS_PER_MINUTE },
|
||||
{ value: '-1', expect: MILLIS_PER_MINUTE },
|
||||
{ value: '0h0m0s', expect: 0 },
|
||||
{ value: '0h0m1s', expect: 1000 },
|
||||
{ value: '0h1m0s', expect: 1000 * 60 },
|
||||
{ value: '1h0m0s', expect: 1000 * 60 * 60 },
|
||||
{ value: '23h0m0s', expect: 1000 * 60 * 60 * 23 },
|
||||
{ value: '12h12m12s', expect: 12 * 1000 + 12 * 60 * 1000 + 12 * 1000 * 60 * 60 },
|
||||
{ value: '12H12M12S', expect: 12 * 1000 + 12 * 60 * 1000 + 12 * 1000 * 60 * 60 },
|
||||
{ value: '2m', expect: 2 * 60 * 1000 },
|
||||
{ value: '1h5s', expect: 1000 * 60 * 60 + 1000 * 5 },
|
||||
{ value: '1h2m', expect: 1000 * 60 * 60 + 1000 * 60 * 2 },
|
||||
{ value: '0h0m1s', expect: MILLIS_PER_SECOND },
|
||||
{ value: '0h1m0s', expect: MILLIS_PER_MINUTE },
|
||||
{ value: '1h0m0s', expect: MILLIS_PER_HOUR },
|
||||
{ value: '23h0m0s', expect: 23 * MILLIS_PER_HOUR },
|
||||
{ value: '12h12m12s', expect: 12 * MILLIS_PER_SECOND + 12 * MILLIS_PER_MINUTE + 12 * MILLIS_PER_HOUR },
|
||||
{ value: '12H12M12S', expect: 12 * MILLIS_PER_SECOND + 12 * MILLIS_PER_MINUTE + 12 * MILLIS_PER_HOUR },
|
||||
{ value: '2m', expect: 2 * MILLIS_PER_MINUTE },
|
||||
{ value: '1h5s', expect: MILLIS_PER_HOUR + 5 * MILLIS_PER_SECOND },
|
||||
{ value: '1h2m', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE },
|
||||
];
|
||||
|
||||
for (const s of testData) {
|
||||
it(`handles ${s.value} to left`, () => {
|
||||
it(`handles ${s.value}`, () => {
|
||||
expect(typeof forgivingStringToMillis(s.value)).toBe('number');
|
||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
||||
});
|
||||
@@ -30,10 +32,10 @@ describe('test forgivingStringToMillis()', () => {
|
||||
|
||||
describe('parses strings correctly', () => {
|
||||
const ts = [
|
||||
{ value: '1.1.1', expect: 60 * 60 * 1000 + 60 * 1000 + 1000 },
|
||||
{ value: '12.1.1', expect: 12 * 60 * 60 * 1000 + 60 * 1000 + 1000 },
|
||||
{ value: '12.55.1', expect: 12 * 60 * 60 * 1000 + 55 * 60 * 1000 + 1000 },
|
||||
{ value: '12.55.40', expect: 12 * 60 * 60 * 1000 + 55 * 60 * 1000 + 40 * 1000 },
|
||||
{ value: '1.1.1', expect: MILLIS_PER_HOUR + MILLIS_PER_MINUTE + MILLIS_PER_SECOND },
|
||||
{ value: '12.1.1', expect: 12 * MILLIS_PER_HOUR + MILLIS_PER_MINUTE + MILLIS_PER_SECOND },
|
||||
{ value: '12.55.1', expect: 12 * MILLIS_PER_HOUR + 55 * MILLIS_PER_MINUTE + MILLIS_PER_SECOND },
|
||||
{ value: '12.55.40', expect: 12 * MILLIS_PER_HOUR + 55 * MILLIS_PER_MINUTE + 40 * MILLIS_PER_SECOND },
|
||||
];
|
||||
|
||||
for (const s of ts) {
|
||||
@@ -45,15 +47,15 @@ describe('test forgivingStringToMillis()', () => {
|
||||
|
||||
describe('parses time strings', () => {
|
||||
const ts = [
|
||||
{ value: '1h2m3s', expect: 60 * 60 * 1000 + 2 * 60 * 1000 + 3 * 1000 },
|
||||
{ value: '1h3s', expect: 60 * 60 * 1000 + 3 * 1000 },
|
||||
{ value: '1h2m', expect: 60 * 60 * 1000 + 2 * 60 * 1000 },
|
||||
{ value: '10h', expect: 10 * 60 * 60 * 1000 },
|
||||
{ value: '10m', expect: 10 * 60 * 1000 },
|
||||
{ value: '10s', expect: 10 * 1000 },
|
||||
{ value: '120h', expect: 120 * 60 * 60 * 1000 },
|
||||
{ value: '120m', expect: 120 * 60 * 1000 },
|
||||
{ value: '120s', expect: 120 * 1000 },
|
||||
{ value: '1h2m3s', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 3 * MILLIS_PER_SECOND },
|
||||
{ value: '1h3s', expect: MILLIS_PER_HOUR + 3 * MILLIS_PER_SECOND },
|
||||
{ value: '1h2m', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE },
|
||||
{ value: '10h', expect: 10 * MILLIS_PER_HOUR },
|
||||
{ value: '10m', expect: 10 * MILLIS_PER_MINUTE },
|
||||
{ value: '10s', expect: 10 * MILLIS_PER_SECOND },
|
||||
{ value: '120h', expect: 120 * MILLIS_PER_HOUR },
|
||||
{ value: '120m', expect: 120 * MILLIS_PER_MINUTE },
|
||||
{ value: '120s', expect: 120 * MILLIS_PER_SECOND },
|
||||
];
|
||||
|
||||
for (const s of ts) {
|
||||
@@ -65,20 +67,20 @@ describe('test forgivingStringToMillis()', () => {
|
||||
|
||||
describe('handles am/pm', () => {
|
||||
const ampm = [
|
||||
{ value: '9:10:11am', expect: 9 * 60 * 60 * 1000 + 10 * 60 * 1000 + 11 * 1000 },
|
||||
{ value: '9:10:11a', expect: 9 * 60 * 60 * 1000 + 10 * 60 * 1000 + 11 * 1000 },
|
||||
{ value: '9:10:11pm', expect: (12 + 9) * 60 * 60 * 1000 + 10 * 60 * 1000 + 11 * 1000 },
|
||||
{ value: '9:10:11p', expect: (12 + 9) * 60 * 60 * 1000 + 10 * 60 * 1000 + 11 * 1000 },
|
||||
{ value: '9:10am', expect: 9 * 60 * 60 * 1000 + 10 * 60 * 1000 },
|
||||
{ value: '9:10a', expect: 9 * 60 * 60 * 1000 + 10 * 60 * 1000 },
|
||||
{ value: '9:10pm', expect: (12 + 9) * 60 * 60 * 1000 + 10 * 60 * 1000 },
|
||||
{ value: '9:10p', expect: (12 + 9) * 60 * 60 * 1000 + 10 * 60 * 1000 },
|
||||
{ value: '9am', expect: 9 * 60 * 60 * 1000 },
|
||||
{ value: '9a', expect: 9 * 60 * 60 * 1000 },
|
||||
{ value: '9pm', expect: (12 + 9) * 60 * 60 * 1000 },
|
||||
{ value: '9p', expect: (12 + 9) * 60 * 60 * 1000 },
|
||||
{ value: '9:10:11am', expect: 9 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 11 * MILLIS_PER_SECOND },
|
||||
{ value: '9:10:11a', expect: 9 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 11 * MILLIS_PER_SECOND },
|
||||
{ value: '9:10:11pm', expect: (12 + 9) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 11 * MILLIS_PER_SECOND },
|
||||
{ value: '9:10:11p', expect: (12 + 9) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 11 * MILLIS_PER_SECOND },
|
||||
{ value: '9:10am', expect: 9 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE },
|
||||
{ value: '9:10a', expect: 9 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE },
|
||||
{ value: '9:10pm', expect: (12 + 9) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE },
|
||||
{ value: '9:10p', expect: (12 + 9) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE },
|
||||
{ value: '9am', expect: 9 * MILLIS_PER_HOUR },
|
||||
{ value: '9a', expect: 9 * MILLIS_PER_HOUR },
|
||||
{ value: '9pm', expect: (12 + 9) * MILLIS_PER_HOUR },
|
||||
{ value: '9p', expect: (12 + 9) * MILLIS_PER_HOUR },
|
||||
{ value: '12am', expect: 0 },
|
||||
{ value: '12pm', expect: 12 * 60 * 60 * 1000 },
|
||||
{ value: '12pm', expect: 12 * MILLIS_PER_HOUR },
|
||||
];
|
||||
|
||||
for (const s of ampm) {
|
||||
@@ -90,12 +92,12 @@ describe('test forgivingStringToMillis()', () => {
|
||||
|
||||
describe('it infers separators when non existent', () => {
|
||||
const testCases = [
|
||||
{ value: '1', expect: 1000 * 60 }, // 00:01:00
|
||||
{ value: '12', expect: 1000 * 60 * 12 }, // 00:12:00
|
||||
{ value: '123', expect: 1000 * 60 * 23 + 1000 * 60 * 60 }, // 01:23:00
|
||||
{ value: '1234', expect: 1000 * 60 * 34 + 1000 * 60 * 60 * 12 }, // 12:34:00
|
||||
{ value: '12345', expect: 1000 * 60 * 34 + 1000 * 60 * 60 * 12 + 5 * 1000 }, // 12:34:05
|
||||
{ value: '123456', expect: 1000 * 60 * 34 + 1000 * 60 * 60 * 12 + 56 * 1000 }, // 12:34:56
|
||||
{ value: '1', expect: MILLIS_PER_MINUTE }, // 00:01:00
|
||||
{ value: '12', expect: 12 * MILLIS_PER_MINUTE }, // 00:12:00
|
||||
{ value: '123', expect: MILLIS_PER_HOUR + 23 * MILLIS_PER_MINUTE }, // 01:23:00
|
||||
{ value: '1234', expect: 12 * MILLIS_PER_HOUR + 34 * MILLIS_PER_MINUTE }, // 12:34:00
|
||||
{ value: '12345', expect: 12 * MILLIS_PER_HOUR + 34 * MILLIS_PER_MINUTE + 5 * MILLIS_PER_SECOND }, // 12:34:05
|
||||
{ value: '123456', expect: 12 * MILLIS_PER_HOUR + 34 * MILLIS_PER_MINUTE + 56 * MILLIS_PER_SECOND }, // 12:34:56
|
||||
];
|
||||
|
||||
for (const s of testCases) {
|
||||
@@ -105,12 +107,12 @@ describe('test forgivingStringToMillis()', () => {
|
||||
}
|
||||
|
||||
const sixDigits = [
|
||||
{ value: '000000', expect: 0 },
|
||||
{ value: '000001', expect: 1000 },
|
||||
{ value: '000100', expect: 1000 * 60 },
|
||||
{ value: '010000', expect: 1000 * 60 * 60 },
|
||||
{ value: '230000', expect: 1000 * 60 * 60 * 23 },
|
||||
{ value: '121212', expect: 12 * 1000 + 12 * 60 * 1000 + 12 * 1000 * 60 * 60 },
|
||||
{ value: '000000', expect: 0 }, // 00:00:00
|
||||
{ value: '000001', expect: MILLIS_PER_SECOND }, // 00:00:01
|
||||
{ value: '000100', expect: MILLIS_PER_MINUTE }, // 00:01:00
|
||||
{ value: '010000', expect: MILLIS_PER_HOUR }, // 01:00:00
|
||||
{ value: '230000', expect: MILLIS_PER_HOUR * 23 }, // 23:00:00
|
||||
{ value: '121212', expect: 12 * MILLIS_PER_HOUR + 12 * MILLIS_PER_MINUTE + 12 * MILLIS_PER_SECOND }, // 12:12:12
|
||||
];
|
||||
|
||||
for (const s of sixDigits) {
|
||||
@@ -121,13 +123,13 @@ describe('test forgivingStringToMillis()', () => {
|
||||
|
||||
const fiveDigits = [
|
||||
{ value: '00000', expect: 0 },
|
||||
{ value: '00001', expect: 1000 }, // 00:00:01
|
||||
{ value: '00010', expect: 1000 * 60 }, // 00:01:00
|
||||
{ value: '00100', expect: 1000 * 60 * 10 }, // 00:10:00
|
||||
{ value: '01000', expect: 1000 * 60 * 60 }, // 01:00:00
|
||||
{ value: '10000', expect: 1000 * 60 * 60 * 10 }, // 10:00:00
|
||||
{ value: '23000', expect: 1000 * 60 * 60 * 23 }, // 23:00:00
|
||||
{ value: '12121', expect: 1000 + 12 * 60 * 1000 + 12 * 1000 * 60 * 60 }, // 12:12:01
|
||||
{ value: '00001', expect: MILLIS_PER_SECOND }, // 00:00:01
|
||||
{ value: '00010', expect: MILLIS_PER_MINUTE }, // 00:01:00
|
||||
{ value: '00100', expect: 10 * MILLIS_PER_MINUTE }, // 00:10:00
|
||||
{ value: '01000', expect: MILLIS_PER_HOUR }, // 01:00:00
|
||||
{ value: '10000', expect: 10 * MILLIS_PER_HOUR }, // 10:00:00
|
||||
{ value: '23000', expect: 23 * MILLIS_PER_HOUR }, // 23:00:00
|
||||
{ value: '12121', expect: 12 * MILLIS_PER_HOUR + 12 * MILLIS_PER_MINUTE + 1 * MILLIS_PER_SECOND }, // 12:12:01
|
||||
];
|
||||
|
||||
for (const s of fiveDigits) {
|
||||
@@ -138,12 +140,12 @@ describe('test forgivingStringToMillis()', () => {
|
||||
|
||||
const fourDigits = [
|
||||
{ value: '0000', expect: 0 },
|
||||
{ value: '0001', expect: 1000 * 60 }, // 00:01:00
|
||||
{ value: '0010', expect: 1000 * 60 * 10 }, // 00:10:00
|
||||
{ value: '0100', expect: 1000 * 60 * 60 }, // 01:00:00
|
||||
{ value: '1000', expect: 1000 * 60 * 60 * 10 }, // 10:00:00
|
||||
{ value: '2300', expect: 1000 * 60 * 60 * 23 }, // 23:00:00
|
||||
{ value: '1212', expect: 12 * 60 * 1000 + 12 * 1000 * 60 * 60 }, // 12:12:00
|
||||
{ value: '0001', expect: MILLIS_PER_MINUTE }, // 00:01:00
|
||||
{ value: '0010', expect: 10 * MILLIS_PER_MINUTE }, // 00:10:00
|
||||
{ value: '0100', expect: MILLIS_PER_HOUR }, // 01:00:00
|
||||
{ value: '1000', expect: 10 * MILLIS_PER_HOUR }, // 10:00:00
|
||||
{ value: '2300', expect: 23 * MILLIS_PER_HOUR }, // 23:00:00
|
||||
{ value: '1212', expect: 12 * MILLIS_PER_HOUR + 12 * MILLIS_PER_MINUTE }, // 12:12:00
|
||||
];
|
||||
|
||||
for (const s of fourDigits) {
|
||||
@@ -154,11 +156,11 @@ describe('test forgivingStringToMillis()', () => {
|
||||
|
||||
const threeDigits = [
|
||||
{ value: '000', expect: 0 },
|
||||
{ value: '001', expect: 1000 * 60 }, // 00:01:00
|
||||
{ value: '010', expect: 1000 * 60 * 10 }, // 00:10:00
|
||||
{ value: '100', expect: 1000 * 60 * 60 }, // 01:00:00
|
||||
{ value: '230', expect: 2 * 1000 * 60 * 60 + 30 * 1000 * 60 }, // 02:30:00
|
||||
{ value: '121', expect: 21 * 60 * 1000 + 1000 * 60 * 60 }, // 01:21:00
|
||||
{ value: '001', expect: MILLIS_PER_MINUTE }, // 00:01:00
|
||||
{ value: '010', expect: 10 * MILLIS_PER_MINUTE }, // 00:10:00
|
||||
{ value: '100', expect: MILLIS_PER_HOUR }, // 01:00:00
|
||||
{ value: '230', expect: 2 * MILLIS_PER_HOUR + 30 * MILLIS_PER_MINUTE }, // 02:30:00
|
||||
{ value: '121', expect: MILLIS_PER_HOUR + 21 * MILLIS_PER_MINUTE }, // 01:21:00
|
||||
];
|
||||
|
||||
for (const s of threeDigits) {
|
||||
@@ -169,9 +171,9 @@ describe('test forgivingStringToMillis()', () => {
|
||||
|
||||
const twoDigits = [
|
||||
{ value: '00', expect: 0 },
|
||||
{ value: '01', expect: 1000 * 60 }, // 00:01:00
|
||||
{ value: '10', expect: 1000 * 60 * 10 }, // 00:10:00
|
||||
{ value: '23', expect: 1000 * 60 * 23 }, // 00:23:00
|
||||
{ value: '01', expect: MILLIS_PER_MINUTE }, // 00:01:00
|
||||
{ value: '10', expect: 10 * MILLIS_PER_MINUTE }, // 00:10:00
|
||||
{ value: '23', expect: 23 * MILLIS_PER_MINUTE }, // 00:23:00
|
||||
];
|
||||
|
||||
for (const s of twoDigits) {
|
||||
@@ -192,18 +194,17 @@ describe('test forgivingStringToMillis()', () => {
|
||||
describe('handles overflows', () => {
|
||||
const ts = [
|
||||
// minutes overflow
|
||||
{ value: '2.0.0', expect: 1000 * 60 * 120 },
|
||||
{ value: '99', expect: 1000 * 60 * 99 },
|
||||
{ value: '1.39.0', expect: 1000 * 60 * 99 },
|
||||
{ value: '99', expect: 99 * MILLIS_PER_MINUTE },
|
||||
{ value: '1.39.0', expect: 99 * MILLIS_PER_MINUTE },
|
||||
// seconds overflow
|
||||
{ value: '0.0.120', expect: 120 * 1000 },
|
||||
{ value: '0.2.0', expect: 120 * 1000 },
|
||||
{ value: '0.0.99', expect: 99 * 1000 },
|
||||
{ value: '0.1.39', expect: 99 * 1000 },
|
||||
{ value: '0.0.120', expect: 120 * MILLIS_PER_SECOND },
|
||||
{ value: '0.2.0', expect: 120 * MILLIS_PER_SECOND },
|
||||
{ value: '0.0.99', expect: 99 * MILLIS_PER_SECOND },
|
||||
{ value: '0.1.39', expect: 99 * MILLIS_PER_SECOND },
|
||||
// hours overflow
|
||||
{ value: '25.0.0', expect: 1000 * 60 * 60 * 25 },
|
||||
{ value: '25.0.0', expect: 25 * MILLIS_PER_HOUR },
|
||||
// hours overflow
|
||||
{ value: '50.0.0', expect: 1000 * 60 * 60 * 50 },
|
||||
{ value: '50.0.0', expect: 50 * MILLIS_PER_HOUR },
|
||||
];
|
||||
|
||||
for (const s of ts) {
|
||||
@@ -216,10 +217,10 @@ describe('test forgivingStringToMillis()', () => {
|
||||
describe('test fillLeft', () => {
|
||||
describe('function handles separators', () => {
|
||||
const testData = [
|
||||
{ value: '1:2:3:10', expect: 3723000 },
|
||||
{ value: '2,10', expect: 2 * 60 * 60 * 1000 + 60 * 10 * 1000 },
|
||||
{ value: '2.10', expect: 2 * 60 * 60 * 1000 + 60 * 10 * 1000 },
|
||||
{ value: '2 10', expect: 2 * 60 * 60 * 1000 + 60 * 10 * 1000 },
|
||||
{ value: '1:2:3:10', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 3 * MILLIS_PER_SECOND }, // 01:02:03
|
||||
{ value: '2,10', expect: 2 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE },
|
||||
{ value: '2.10', expect: 2 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE },
|
||||
{ value: '2 10', expect: 2 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE },
|
||||
];
|
||||
|
||||
for (const s of testData) {
|
||||
@@ -232,8 +233,8 @@ describe('test forgivingStringToMillis()', () => {
|
||||
|
||||
describe('parses strings correctly', () => {
|
||||
const ts = [
|
||||
{ value: '1.2', expect: 60 * 60 * 1000 + 2 * 60 * 1000 },
|
||||
{ value: '1.70', expect: 60 * 60 * 1000 + 70 * 60 * 1000 },
|
||||
{ value: '1.2', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE },
|
||||
{ value: '1.70', expect: MILLIS_PER_HOUR + 70 * MILLIS_PER_MINUTE },
|
||||
];
|
||||
|
||||
for (const s of ts) {
|
||||
@@ -246,8 +247,8 @@ describe('test forgivingStringToMillis()', () => {
|
||||
describe('handles overflows', () => {
|
||||
const ts = [
|
||||
// minutes overflow
|
||||
{ value: '0.120', expect: 120 * 60 * 1000 },
|
||||
{ value: '0.99', expect: 99 * 60 * 1000 },
|
||||
{ value: '0.120', expect: 120 * MILLIS_PER_MINUTE },
|
||||
{ value: '0.99', expect: 99 * MILLIS_PER_MINUTE },
|
||||
];
|
||||
|
||||
for (const s of ts) {
|
||||
|
||||
+2
-2
@@ -93,10 +93,10 @@ export default function UrlPresetsForm() {
|
||||
<Panel.SubHeader>
|
||||
URL presets
|
||||
<div className={style.actionButtons}>
|
||||
<Button variant='ontime-ghosted' size='md' onClick={onReset} isDisabled={!canSubmit}>
|
||||
<Button variant='ontime-ghosted' size='sm' onClick={onReset} isDisabled={!canSubmit}>
|
||||
Revert to saved
|
||||
</Button>
|
||||
<Button variant='ontime-filled' size='md' type='submit' isDisabled={!canSubmit} isLoading={isSubmitting}>
|
||||
<Button variant='ontime-filled' size='sm' type='submit' isDisabled={!canSubmit} isLoading={isSubmitting}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -169,7 +169,7 @@ function ActionMenu({
|
||||
<MenuItem onClick={handleRename}>Rename</MenuItem>
|
||||
<MenuItem onClick={handleDuplicate}>Duplicate</MenuItem>
|
||||
<MenuItem onClick={handleDownload}>Download</MenuItem>
|
||||
{current && <MenuItem onClick={handleExportCSV}>Export CSV Rundown</MenuItem>}
|
||||
<MenuItem onClick={handleExportCSV}>Export CSV Rundown</MenuItem>
|
||||
<MenuItem isDisabled={current} onClick={handleDelete}>
|
||||
Delete
|
||||
</MenuItem>
|
||||
|
||||
@@ -95,7 +95,7 @@ function TitlesOverview() {
|
||||
}
|
||||
|
||||
function TimerOverview() {
|
||||
const {current} = useTimer();
|
||||
const { current } = useTimer();
|
||||
|
||||
const display = millisToString(current);
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { dayInMs } from 'ontime-utils';
|
||||
|
||||
import { calculateEndAndDaySpan } from '../overviewUtils';
|
||||
|
||||
describe('calculateEndAndDaySpan', () => {
|
||||
it('should return [null, 0] when end is null', () => {
|
||||
const result = calculateEndAndDaySpan(null);
|
||||
expect(result).toEqual([null, 0]);
|
||||
});
|
||||
|
||||
it('should return [end, 0] when end is less than or equal to dayInMs', () => {
|
||||
const end = dayInMs / 2;
|
||||
const result = calculateEndAndDaySpan(end);
|
||||
expect(result).toEqual([end, 0]);
|
||||
});
|
||||
|
||||
it('should return [end % dayInMs, Math.floor(end / dayInMs)] when end is greater than dayInMs', () => {
|
||||
const end = dayInMs * 1.5;
|
||||
const result = calculateEndAndDaySpan(end);
|
||||
expect(result).toEqual([end % dayInMs, Math.floor(end / dayInMs)]);
|
||||
});
|
||||
});
|
||||
@@ -13,20 +13,14 @@ export function formatedTime(time: MaybeNumber) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates a day span from a number range
|
||||
* @param end
|
||||
* @returns
|
||||
* Calculates how long a time is and how many days it spans
|
||||
*/
|
||||
export function calculateEndAndDaySpan(end: MaybeNumber): [MaybeNumber, number] {
|
||||
let maybeEnd = end;
|
||||
let maybeDaySpan = 0;
|
||||
if (end !== null) {
|
||||
if (end > dayInMs) {
|
||||
maybeEnd = end % dayInMs;
|
||||
maybeDaySpan = Math.floor(end / dayInMs);
|
||||
}
|
||||
if (end !== null && end > dayInMs) {
|
||||
return [end % dayInMs, Math.floor(end / dayInMs)];
|
||||
}
|
||||
return [maybeEnd, maybeDaySpan];
|
||||
|
||||
return [end, 0];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -242,13 +242,7 @@ export default function EventBlock(props: EventBlockProps) {
|
||||
onContextMenu={onContextMenu}
|
||||
id='event-block'
|
||||
>
|
||||
<RundownIndicators
|
||||
timeStart={timeStart}
|
||||
timeEnd={timeEnd}
|
||||
previousStart={previousStart}
|
||||
previousEnd={previousEnd}
|
||||
delay={delay}
|
||||
/>
|
||||
<RundownIndicators timeStart={timeStart} previousStart={previousStart} previousEnd={previousEnd} delay={delay} />
|
||||
|
||||
<div className={style.binder} style={{ ...binderColours }} tabIndex={-1}>
|
||||
<span className={style.drag} ref={handleRef} {...dragAttributes} {...dragListeners}>
|
||||
|
||||
@@ -10,24 +10,41 @@ export function formatDelay(timeStart: number, delay: number): string | undefine
|
||||
return `New start ${timeTag}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function checks whether a given event is the day after from its predecessor
|
||||
* We consider an event to be the day after, if it begins before the start of the previous
|
||||
* @example day after
|
||||
* // 09:00 - 10:00
|
||||
* // 08:00 - 10:30 <--- day after
|
||||
* @example same day
|
||||
* // 09:00 - 10:00
|
||||
* // 09:30 - 10:30 <--- same day
|
||||
*/
|
||||
function checkIsNextDay(previousStart: number, timeStart: number): boolean {
|
||||
return timeStart < previousStart;
|
||||
}
|
||||
|
||||
export function formatOverlap(
|
||||
previousStart: MaybeNumber,
|
||||
previousEnd: MaybeNumber,
|
||||
timeStart: number,
|
||||
timeEnd: number,
|
||||
): string | undefined {
|
||||
if (previousEnd === null) return;
|
||||
const noPreviousElement = previousEnd === null || previousStart === null;
|
||||
if (noPreviousElement) return;
|
||||
|
||||
const overlap = previousEnd - timeStart;
|
||||
if (overlap === 0) return;
|
||||
|
||||
if (previousStart && timeStart < previousEnd) {
|
||||
const overlap = timeEnd - previousStart;
|
||||
if (overlap > 0) {
|
||||
const overlapString = removeLeadingZero(millisToString(Math.abs(overlap)));
|
||||
return `Overlap ${overlapString}`;
|
||||
}
|
||||
const gap = timeStart + dayInMs - previousEnd;
|
||||
const previousCrossMidnight = previousStart > previousEnd;
|
||||
const isNextDay = previousCrossMidnight
|
||||
? checkIsNextDay(previousEnd, timeStart) || previousEnd == 0 // exception for when previousEnd is precisely midnight
|
||||
: checkIsNextDay(previousStart, timeStart);
|
||||
|
||||
const correctedPreviousEnd = previousCrossMidnight ? previousEnd + dayInMs : previousEnd;
|
||||
|
||||
if (isNextDay) {
|
||||
const gap = dayInMs - correctedPreviousEnd + timeStart;
|
||||
if (gap === 0) return;
|
||||
const gapString = removeLeadingZero(millisToString(Math.abs(gap)));
|
||||
return `Gap ${gapString} (next day)`;
|
||||
}
|
||||
|
||||
@@ -6,16 +6,15 @@ import style from './RundownIndicators.module.scss';
|
||||
|
||||
interface RundownIndicatorProps {
|
||||
timeStart: number;
|
||||
timeEnd: number;
|
||||
previousStart: MaybeNumber;
|
||||
previousEnd: MaybeNumber;
|
||||
delay: number;
|
||||
}
|
||||
|
||||
export default function RundownIndicators(props: RundownIndicatorProps) {
|
||||
const { timeStart, timeEnd, previousStart, previousEnd, delay } = props;
|
||||
const { timeStart, previousStart, previousEnd, delay } = props;
|
||||
|
||||
const hasOverlap = formatOverlap(previousStart, previousEnd, timeStart, timeEnd);
|
||||
const hasOverlap = formatOverlap(previousStart, previousEnd, timeStart);
|
||||
const hasDelay = formatDelay(timeStart, delay);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { MILLIS_PER_HOUR } from 'ontime-utils';
|
||||
|
||||
import { formatDelay, formatOverlap } from '../EventBlock.utils';
|
||||
|
||||
describe('formatDelay()', () => {
|
||||
@@ -14,35 +16,47 @@ describe('formatOverlap()', () => {
|
||||
const previousStart = 0;
|
||||
const previousEnd = 60000; // 1 min
|
||||
const timeStart = 30000; // 30 sec
|
||||
const timeEnd = 90000; // 1:30 min
|
||||
const result = formatOverlap(previousStart, previousEnd, timeStart, timeEnd);
|
||||
const result = formatOverlap(previousStart, previousEnd, timeStart);
|
||||
expect(result).toEqual('Overlap 0:30');
|
||||
});
|
||||
|
||||
it('bug #949 recognises an overlap between two times', () => {
|
||||
const previousStart = 46800000; // 13:00:00
|
||||
const previousEnd = 48600000; // 13:30:00
|
||||
const timeStart = 48300000; // 13:25:00
|
||||
const result = formatOverlap(previousStart, previousEnd, timeStart);
|
||||
expect(result).toEqual('Overlap 5:00');
|
||||
});
|
||||
|
||||
it('handles events the day after, without overlap', () => {
|
||||
const previousStart = new Date(0).setUTCHours(11);
|
||||
const previousEnd = new Date(0).setUTCHours(12);
|
||||
const timeStart = new Date(0).setUTCHours(6);
|
||||
const timeEnd = new Date(0).setUTCHours(10);
|
||||
const result = formatOverlap(previousStart, previousEnd, timeStart, timeEnd);
|
||||
const previousStart = 11 * MILLIS_PER_HOUR;
|
||||
const previousEnd = 12 * MILLIS_PER_HOUR;
|
||||
const timeStart = 6 * MILLIS_PER_HOUR;
|
||||
const result = formatOverlap(previousStart, previousEnd, timeStart);
|
||||
expect(result).toBe('Gap 18:00:00 (next day)');
|
||||
});
|
||||
|
||||
it('handles events the day after, with overlap', () => {
|
||||
const previousStart = new Date(0).setUTCHours(9);
|
||||
const previousEnd = new Date(0).setUTCHours(10);
|
||||
const timeStart = new Date(0).setUTCHours(6);
|
||||
const timeEnd = new Date(0).setUTCHours(11);
|
||||
const result = formatOverlap(previousStart, previousEnd, timeStart, timeEnd);
|
||||
expect(result).toBe('Overlap 02:00:00');
|
||||
});
|
||||
|
||||
it('handles events the day after, with gap', () => {
|
||||
const previousStart = new Date(0).setUTCHours(17);
|
||||
const previousEnd = new Date(0).setUTCHours(23);
|
||||
const timeStart = new Date(0).setUTCHours(9);
|
||||
const timeEnd = new Date(0).setUTCHours(11);
|
||||
const result = formatOverlap(previousStart, previousEnd, timeStart, timeEnd);
|
||||
const previousStart = 17 * MILLIS_PER_HOUR;
|
||||
const previousEnd = 23 * MILLIS_PER_HOUR;
|
||||
const timeStart = 9 * MILLIS_PER_HOUR;
|
||||
const result = formatOverlap(previousStart, previousEnd, timeStart);
|
||||
expect(result).toBe('Gap 10:00:00 (next day)');
|
||||
});
|
||||
|
||||
it('handles events the day after, with previous ending at midnight', () => {
|
||||
const previousStart = 23 * MILLIS_PER_HOUR; // 23:00:00
|
||||
const previousEnd = 0; // 00:00:00
|
||||
const timeStart = 1 * MILLIS_PER_HOUR; // 01:00:00
|
||||
const result = formatOverlap(previousStart, previousEnd, timeStart);
|
||||
expect(result).toBe('Gap 01:00:00 (next day)');
|
||||
});
|
||||
|
||||
it('handles events the day after, with previous ending over midnight', () => {
|
||||
const previousStart = 23 * MILLIS_PER_HOUR;
|
||||
const previousEnd = 1 * MILLIS_PER_HOUR;
|
||||
const timeStart = 2 * MILLIS_PER_HOUR;
|
||||
const result = formatOverlap(previousStart, previousEnd, timeStart);
|
||||
expect(result).toBe('Gap 01:00:00');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,3 +32,9 @@
|
||||
.prompt {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.divider {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
@@ -18,49 +18,77 @@ function EventEditorEmpty() {
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<Kbd>{deviceAlt}</Kbd> + <Kbd>↑</Kbd> / <Kbd>↓</Kbd>
|
||||
<Kbd>{deviceAlt}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>↑</Kbd>
|
||||
<AuxKey>/</AuxKey>
|
||||
<Kbd>↓</Kbd>
|
||||
</td>
|
||||
<td>Select entry</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Kbd>{deviceAlt}</Kbd> + <Kbd>{deviceMod}</Kbd> + <Kbd>↑</Kbd> / <Kbd>↑</Kbd>
|
||||
<Kbd>{deviceAlt}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>{deviceMod}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>↑</Kbd>
|
||||
<AuxKey>/</AuxKey>
|
||||
<Kbd>↑</Kbd>
|
||||
</td>
|
||||
<td>Reorder selected entry</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Kbd>{deviceAlt}</Kbd> + <Kbd>E</Kbd>
|
||||
<Kbd>{deviceAlt}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>E</Kbd>
|
||||
</td>
|
||||
<td>Add event below</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Kbd>{deviceAlt}</Kbd> + <Kbd>↑</Kbd> + <Kbd>E</Kbd>
|
||||
<Kbd>{deviceAlt}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>↑</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>E</Kbd>
|
||||
</td>
|
||||
<td>Add event above</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Kbd>{deviceAlt}</Kbd> + <Kbd>B</Kbd>
|
||||
<Kbd>{deviceAlt}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>B</Kbd>
|
||||
</td>
|
||||
<td>Add block below</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Kbd>{deviceAlt}</Kbd> + <Kbd>↑</Kbd> + <Kbd>B</Kbd>
|
||||
<Kbd>{deviceAlt}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>↑</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>B</Kbd>
|
||||
</td>
|
||||
<td>Add block above</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Kbd>{deviceAlt}</Kbd> + <Kbd>D</Kbd>
|
||||
<Kbd>{deviceAlt}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>D</Kbd>
|
||||
</td>
|
||||
<td>Add delay below</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Kbd>{deviceAlt}</Kbd> + <Kbd>↑</Kbd> + <Kbd>D</Kbd>
|
||||
<Kbd>{deviceAlt}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>↑</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>D</Kbd>
|
||||
</td>
|
||||
<td>Add delay above</td>
|
||||
</tr>
|
||||
@@ -72,25 +100,35 @@ function EventEditorEmpty() {
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Kbd>{deviceMod}</Kbd> + <Kbd>⌫</Kbd>
|
||||
<Kbd>{deviceMod}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>⌫</Kbd>
|
||||
</td>
|
||||
<td>Delete selected entry</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Kbd>{deviceMod}</Kbd> + <Kbd>C</Kbd>
|
||||
<Kbd>{deviceMod}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>C</Kbd>
|
||||
</td>
|
||||
<td>Copy selected entry</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Kbd>{deviceMod}</Kbd> + <Kbd>↑</Kbd> + <Kbd>V</Kbd>
|
||||
<Kbd>{deviceMod}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>↑</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>V</Kbd>
|
||||
</td>
|
||||
<td>Paste above</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Kbd>{deviceMod}</Kbd> + <Kbd>V</Kbd>
|
||||
<Kbd>{deviceMod}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>V</Kbd>
|
||||
</td>
|
||||
<td>Paste below</td>
|
||||
</tr>
|
||||
@@ -100,3 +138,7 @@ function EventEditorEmpty() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AuxKey({ children }: { children: React.ReactNode }) {
|
||||
return <span className={style.divider}>{children}</span>;
|
||||
}
|
||||
|
||||
@@ -83,9 +83,13 @@ const theme = extendTheme({
|
||||
Kbd: {
|
||||
baseStyle: {
|
||||
borderRadius: '2px',
|
||||
border: '1px solid rgba(255, 255, 255, 0.20)',
|
||||
border: 'none',
|
||||
background: 'rgba(255, 255, 255, 0.10)',
|
||||
color: 'white',
|
||||
padding: '0.125rem 0.5rem',
|
||||
color: '#f6f6f6', // $ui-white
|
||||
fontWeight: 400,
|
||||
boxShadow: '0px 0px 3px 0px rgba(0,0,0,0.4)',
|
||||
fontSize: 'calc(1rem - 2px)',
|
||||
},
|
||||
},
|
||||
Menu: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime",
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.2",
|
||||
"author": "Carlos Valente",
|
||||
"description": "Time keeping for live events",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "ontime-server",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.2",
|
||||
"exports": "./src/index.js",
|
||||
"dependencies": {
|
||||
"@googleapis/sheets": "^5.0.5",
|
||||
@@ -10,9 +10,8 @@
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.0.1",
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.17.3",
|
||||
"express-static-gzip": "^2.1.7",
|
||||
"express-validator": "^6.14.2",
|
||||
"express-validator": "^7.0.1",
|
||||
"fast-equals": "^5.0.1",
|
||||
"google-auth-library": "^9.4.2",
|
||||
"got": "^14.0.0",
|
||||
|
||||
@@ -6,17 +6,19 @@ import {
|
||||
ProjectData,
|
||||
ProjectFileListResponse,
|
||||
} from 'ontime-types';
|
||||
import { getErrorMessage } from 'ontime-utils';
|
||||
|
||||
import { join } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import type { Request, Response } from 'express';
|
||||
|
||||
import { failEmptyObjects } from '../../utils/routerUtils.js';
|
||||
import { resolveDbPath, resolveProjectsDirectory } from '../../setup/index.js';
|
||||
import { resolveDbDirectory, resolveProjectsDirectory } from '../../setup/index.js';
|
||||
|
||||
import * as projectService from '../../services/project-service/ProjectService.js';
|
||||
import { ensureJsonExtension } from '../../utils/fileManagement.js';
|
||||
import { generateUniqueFileName } from '../../utils/generateUniqueFilename.js';
|
||||
import { appStateService } from '../../services/app-state-service/AppStateService.js';
|
||||
import { getErrorMessage } from 'ontime-utils';
|
||||
|
||||
export async function patchPartialProjectFile(req: Request, res: Response<DatabaseModel | ErrorResponse>) {
|
||||
// all fields are optional in validation
|
||||
@@ -76,9 +78,30 @@ export async function createProjectFile(req: Request, res: Response<{ filename:
|
||||
}
|
||||
}
|
||||
|
||||
export async function projectDownload(_req: Request, res: Response) {
|
||||
const fileTitle = projectService.getProjectTitle();
|
||||
res.download(resolveDbPath, `${fileTitle}.json`, (error) => {
|
||||
/**
|
||||
* Utility function finds the correct project file to download
|
||||
*/
|
||||
function selectProjectFile(fileName?: string) {
|
||||
const projectsDirectory = resolveDbDirectory;
|
||||
const fileToDownload = fileName ? ensureJsonExtension(fileName) : projectService.getProjectTitle();
|
||||
const pathToFile = join(projectsDirectory, fileToDownload);
|
||||
|
||||
return { pathToFile, name: fileToDownload };
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows downloading of a optionally given project files
|
||||
* If no {filename} is provided, loaded file will be served
|
||||
*/
|
||||
export async function projectDownload(req: Request, res: Response) {
|
||||
const { pathToFile, name } = selectProjectFile(req.body?.fileName);
|
||||
|
||||
// Check if the file exists before attempting to download
|
||||
if (!existsSync(pathToFile)) {
|
||||
return res.status(404).send({ message: `Project ${name} not found.` });
|
||||
}
|
||||
|
||||
res.download(pathToFile, name, (error) => {
|
||||
if (error) {
|
||||
const message = getErrorMessage(error);
|
||||
res.status(500).send({ message });
|
||||
|
||||
@@ -16,6 +16,7 @@ import { uploadProjectFile } from './db.middleware.js';
|
||||
import {
|
||||
projectSanitiser,
|
||||
sanitizeProjectFilename,
|
||||
validateDownloadProject,
|
||||
validateLoadProjectFile,
|
||||
validatePatchProjectFile,
|
||||
validateProjectDuplicate,
|
||||
@@ -24,7 +25,7 @@ import {
|
||||
|
||||
export const router = express.Router();
|
||||
|
||||
router.get('/download', projectDownload);
|
||||
router.post('/download', validateDownloadProject, projectDownload);
|
||||
router.post('/upload', uploadProjectFile, postProjectFile);
|
||||
|
||||
router.patch('/', validatePatchProjectFile, patchPartialProjectFile);
|
||||
|
||||
@@ -104,3 +104,19 @@ export const validateProjectRename = [
|
||||
next();
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* @description Validates a download request which can include an optional project name.
|
||||
*/
|
||||
export const validateDownloadProject = [
|
||||
body('fileName').isString().optional(),
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(422).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
next();
|
||||
},
|
||||
];
|
||||
|
||||
@@ -2,5 +2,5 @@ export const timerConfig = {
|
||||
skipLimit: 1000, // threshold of skip for recalculating
|
||||
updateRate: 32, // how often do we update the timer
|
||||
notificationRate: 1000, // how often do we notify clients and integrations
|
||||
triggerAhead: 16, // how far ahead do we trigger the end event
|
||||
triggerAhead: 10, // how far ahead do we trigger the end event
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { OntimeEvent } from 'ontime-types';
|
||||
|
||||
import * as runtimeState from '../stores/runtimeState.js';
|
||||
import type { UpdateResult } from '../stores/runtimeState.js';
|
||||
import { timerConfig } from '../config/config.js';
|
||||
|
||||
/**
|
||||
* Service manages Ontime's main timer
|
||||
@@ -41,7 +42,7 @@ export class TimerService {
|
||||
}
|
||||
|
||||
const state = runtimeState.getState();
|
||||
const endTime = state.timer.current - 10;
|
||||
const endTime = state.timer.current - timerConfig.triggerAhead;
|
||||
this.endCallback = setTimeout(() => this.update(), endTime);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,24 @@ export class HttpIntegration implements IIntegration<HttpSubscription, HttpSetti
|
||||
|
||||
emit(path: string) {
|
||||
got.get(path, { retry: { limit: 0 } }).catch((err) => {
|
||||
logger.error(LogOrigin.Tx, `HTTP Integration: ${err.code}`);
|
||||
logger.warning(LogOrigin.Tx, `HTTP Integration: ${err.message}`);
|
||||
|
||||
if (err.code === 'ECONNREFUSED') {
|
||||
logger.warning(LogOrigin.Tx, `HTTP Integration: '${err.code}' The server refused the connection`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (err.code === 'ENOTFOUND') {
|
||||
logger.warning(LogOrigin.Tx, `HTTP Integration: '${err.code}' DNS lookup failed`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (err.code === 'ETIMEDOUT') {
|
||||
logger.warning(LogOrigin.Tx, `HTTP Integration: '${err.code}' The connection timed out`);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.warning(LogOrigin.Tx, `HTTP Integration: ${err.code}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +80,8 @@ export function generate(
|
||||
links = {};
|
||||
firstStart = null;
|
||||
lastEnd = null;
|
||||
totalDuration = 0;
|
||||
totalDelay = 0;
|
||||
|
||||
let accumulatedDelay = 0;
|
||||
let daySpan = 0;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EndAction, LogOrigin, OntimeEvent, Playback, RuntimeStore, TimerLifeCycle } from 'ontime-types';
|
||||
import { EndAction, LogOrigin, MaybeNumber, OntimeEvent, Playback, RuntimeStore, TimerLifeCycle } from 'ontime-types';
|
||||
import { millisToString, validatePlayback } from 'ontime-utils';
|
||||
|
||||
import { deepEqual } from 'fast-equals';
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
getPlayableEvents,
|
||||
} from '../rundown-service/rundownUtils.js';
|
||||
import { integrationService } from '../integration-service/IntegrationService.js';
|
||||
import { getForceUpdate, getShouldClockUpdate, getShouldTimerUpdate } from './rundownService.utils.js';
|
||||
|
||||
/**
|
||||
* Service manages runtime status of app
|
||||
@@ -27,15 +28,20 @@ import { integrationService } from '../integration-service/IntegrationService.js
|
||||
*/
|
||||
class RuntimeService {
|
||||
private eventTimer: TimerService | null = null;
|
||||
private lastOnUpdate = -1;
|
||||
private lastIntegrationClockUpdate = -1;
|
||||
private lastIntegrationTimerValue = -1;
|
||||
|
||||
/** last time we updated the socket */
|
||||
static previousUpdate: number;
|
||||
static previousTimerUpdate: number;
|
||||
static previousTimerValue: MaybeNumber;
|
||||
static previousClockUpdate: number;
|
||||
/** last known state */
|
||||
static previousState: RuntimeState;
|
||||
|
||||
constructor() {
|
||||
RuntimeService.previousUpdate = -1;
|
||||
RuntimeService.previousTimerUpdate = -1;
|
||||
RuntimeService.previousTimerValue = -1;
|
||||
RuntimeService.previousClockUpdate = -1;
|
||||
RuntimeService.previousState = {} as RuntimeState;
|
||||
}
|
||||
|
||||
@@ -61,19 +67,25 @@ class RuntimeService {
|
||||
}
|
||||
}
|
||||
|
||||
// update normal cycle
|
||||
if (newState.clock - this.lastOnUpdate >= timerConfig.notificationRate) {
|
||||
const hasRunningTimer = Boolean(newState.eventNow) && newState.timer.playback === Playback.Play;
|
||||
if (hasRunningTimer) {
|
||||
process.nextTick(() => {
|
||||
integrationService.dispatch(TimerLifeCycle.onUpdate);
|
||||
});
|
||||
}
|
||||
const hasRunningTimer = Boolean(newState.eventNow) && newState.timer.playback === Playback.Play;
|
||||
const shouldUpdateTimer =
|
||||
hasRunningTimer && getShouldTimerUpdate(this.lastIntegrationTimerValue, newState.timer.current);
|
||||
|
||||
if (shouldUpdateTimer) {
|
||||
process.nextTick(() => {
|
||||
integrationService.dispatch(TimerLifeCycle.onUpdate);
|
||||
});
|
||||
|
||||
this.lastIntegrationTimerValue = newState.timer.current;
|
||||
}
|
||||
|
||||
const shouldUpdateClock = getShouldClockUpdate(this.lastIntegrationClockUpdate, newState.clock);
|
||||
if (shouldUpdateClock) {
|
||||
process.nextTick(() => {
|
||||
integrationService.dispatch(TimerLifeCycle.onClock);
|
||||
});
|
||||
|
||||
this.lastOnUpdate = newState.clock;
|
||||
this.lastIntegrationClockUpdate = newState.clock;
|
||||
}
|
||||
|
||||
if (shouldCallRoll) {
|
||||
@@ -516,29 +528,30 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
|
||||
// we do the comparison by explicitly for each property
|
||||
// to apply custom logic for different datasets
|
||||
|
||||
// some of the data, we only update at intervals
|
||||
const isTimeToUpdate =
|
||||
state.clock < RuntimeService.previousUpdate ||
|
||||
state.clock - RuntimeService.previousUpdate >= timerConfig.notificationRate;
|
||||
const shouldUpdateClock = getShouldClockUpdate(RuntimeService.previousClockUpdate, state.clock);
|
||||
const shouldForceTimerUpdate = getForceUpdate(RuntimeService.previousTimerUpdate, state.clock);
|
||||
const shouldUpdateTimer =
|
||||
shouldForceTimerUpdate || getShouldTimerUpdate(RuntimeService.previousTimerValue, state.timer.current);
|
||||
|
||||
// some changes need an immediate update
|
||||
const hasNewLoaded = state.eventNow?.id !== RuntimeService.previousState?.eventNow?.id;
|
||||
|
||||
const hasSkippedBack = state.clock < RuntimeService.previousUpdate;
|
||||
const justStarted = !RuntimeService.previousState?.timer;
|
||||
const hasChangedPlayback = RuntimeService.previousState.timer?.playback !== state.timer.playback;
|
||||
const hasImmediateChanges = hasNewLoaded || hasSkippedBack || justStarted || hasChangedPlayback;
|
||||
const hasImmediateChanges = hasNewLoaded || justStarted || hasChangedPlayback;
|
||||
|
||||
if (hasChangedPlayback) {
|
||||
eventStore.set('onAir', state.timer.playback !== Playback.Stop);
|
||||
}
|
||||
|
||||
if (hasImmediateChanges || (isTimeToUpdate && !deepEqual(RuntimeService.previousState?.timer, state.timer))) {
|
||||
if (hasImmediateChanges || (shouldUpdateTimer && !deepEqual(RuntimeService.previousState?.timer, state.timer))) {
|
||||
RuntimeService.previousTimerUpdate = state.clock;
|
||||
RuntimeService.previousTimerValue = state.timer.current;
|
||||
eventStore.set('timer', state.timer);
|
||||
RuntimeService.previousState.timer = { ...state.timer };
|
||||
}
|
||||
|
||||
if (hasChangedPlayback || (isTimeToUpdate && !deepEqual(RuntimeService.previousState?.runtime, state.runtime))) {
|
||||
if (hasChangedPlayback || (shouldUpdateTimer && !deepEqual(RuntimeService.previousState?.runtime, state.runtime))) {
|
||||
eventStore.set('runtime', state.runtime);
|
||||
RuntimeService.previousState.runtime = { ...state.runtime };
|
||||
}
|
||||
@@ -549,8 +562,8 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
|
||||
updateEventIfChanged('eventNext', state);
|
||||
updateEventIfChanged('publicEventNext', state);
|
||||
|
||||
if (isTimeToUpdate) {
|
||||
RuntimeService.previousUpdate = state.clock;
|
||||
if (shouldUpdateClock) {
|
||||
RuntimeService.previousClockUpdate = state.clock;
|
||||
eventStore.set('clock', state.clock);
|
||||
saveRestoreState(state);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { MILLIS_PER_MINUTE } from 'ontime-utils';
|
||||
import { getShouldClockUpdate, getShouldTimerUpdate } from '../rundownService.utils.js';
|
||||
|
||||
describe('getShouldClockUpdate()', () => {
|
||||
it('should return true when we slid forwards', () => {
|
||||
const previousUpdate = Date.now() - 2000; // 2 seconds ago
|
||||
const now = Date.now();
|
||||
const result = getShouldClockUpdate(previousUpdate, now);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when we slid backwards', () => {
|
||||
const previousUpdate = Date.now();
|
||||
const now = Date.now() - 2000; // 2 seconds ago
|
||||
const result = getShouldClockUpdate(previousUpdate, now);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when clock is a second ahead', () => {
|
||||
const previousUpdate = MILLIS_PER_MINUTE - 100;
|
||||
const now = MILLIS_PER_MINUTE;
|
||||
const result = getShouldClockUpdate(previousUpdate, now);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when clock is not a second ahead and force update is not required', () => {
|
||||
const previousUpdate = Date.now() - 32;
|
||||
const now = Date.now();
|
||||
const result = getShouldClockUpdate(previousUpdate, now);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getShouldTimerUpdate', () => {
|
||||
it('should return false when currentValue is null', () => {
|
||||
const previousValue = 0;
|
||||
const currentValue = null;
|
||||
const result = getShouldTimerUpdate(previousValue, currentValue);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when timer is a second ahead', () => {
|
||||
const previousValue = 6500;
|
||||
const currentValue = 5000;
|
||||
const result = getShouldTimerUpdate(previousValue, currentValue);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when timer is not a second ahead', () => {
|
||||
const previousValue = 5500;
|
||||
const currentValue = 5200;
|
||||
const result = getShouldTimerUpdate(previousValue, currentValue);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('timer value is ceiled', () => {
|
||||
const previousValue = 5001; // 6
|
||||
const currentValue = 4999; // 5
|
||||
const result = getShouldTimerUpdate(previousValue, currentValue);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { millisToSeconds } from 'ontime-utils';
|
||||
|
||||
import { timerConfig } from '../../config/config.js';
|
||||
import { MaybeNumber } from 'ontime-types';
|
||||
|
||||
/**
|
||||
* Checks whether we should update the clock value
|
||||
* - clock has slid
|
||||
* - we have rolled into a new seconds unit
|
||||
*/
|
||||
export function getShouldClockUpdate(previousUpdate: number, now: number): boolean {
|
||||
const shouldForceUpdate = getForceUpdate(previousUpdate, now);
|
||||
if (shouldForceUpdate) {
|
||||
return true;
|
||||
}
|
||||
const isClockSecondAhead = millisToSeconds(now) !== millisToSeconds(previousUpdate + timerConfig.triggerAhead);
|
||||
return isClockSecondAhead;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether we should update the timer value
|
||||
* - we have rolled into a new seconds unit
|
||||
*/
|
||||
export function getShouldTimerUpdate(previousValue: number, currentValue: MaybeNumber): boolean {
|
||||
if (currentValue === null) {
|
||||
return false;
|
||||
}
|
||||
// we avoid trigger ahead since it can cause duplicate triggers
|
||||
// we force the timer value to be negative because we need a ceiling reduction
|
||||
const shouldUpdateTimer = millisToSeconds(-currentValue) !== millisToSeconds(-previousValue);
|
||||
return shouldUpdateTimer;
|
||||
}
|
||||
|
||||
/**
|
||||
* In some cases we want to force an update to the timer
|
||||
* - if the clock has slid back
|
||||
* - if we have escaped the update rate (clock slid forward)
|
||||
*/
|
||||
export function getForceUpdate(previousUpdate: number, now: number): boolean {
|
||||
const isClockBehind = now < previousUpdate;
|
||||
const hasExceededRate = now - previousUpdate >= timerConfig.notificationRate;
|
||||
return isClockBehind || hasExceededRate;
|
||||
}
|
||||
@@ -168,7 +168,7 @@ describe('mutation on runtimeState', () => {
|
||||
load(event2, [event1, event2]);
|
||||
start();
|
||||
newState = getState();
|
||||
expect(newState.runtime.actualStart).toBe(firstStart);
|
||||
expect(newState.runtime.actualStart).toBeCloseTo(firstStart, 0);
|
||||
// we are over-under, the difference between the schedule and the actual start
|
||||
const delayBefore = event2.timeStart - newState.clock;
|
||||
expect(newState.runtime.offset).toBe(delayBefore);
|
||||
|
||||
@@ -23,7 +23,7 @@ const initialRuntime: Runtime = {
|
||||
plannedEnd: 0,
|
||||
actualStart: null,
|
||||
expectedEnd: null,
|
||||
};
|
||||
} as const;
|
||||
|
||||
const initialTimer: TimerState = {
|
||||
addedTime: 0,
|
||||
@@ -35,7 +35,7 @@ const initialTimer: TimerState = {
|
||||
playback: Playback.Stop,
|
||||
secondaryTimer: null,
|
||||
startedAt: null,
|
||||
};
|
||||
} as const;
|
||||
|
||||
export type RuntimeState = {
|
||||
clock: number; // realtime clock
|
||||
@@ -59,7 +59,7 @@ const runtimeState: RuntimeState = {
|
||||
publicEventNow: null,
|
||||
eventNext: null,
|
||||
publicEventNext: null,
|
||||
runtime: initialRuntime,
|
||||
runtime: { ...initialRuntime },
|
||||
timer: { ...initialTimer },
|
||||
_timer: {
|
||||
totalDelay: 0,
|
||||
@@ -78,12 +78,10 @@ export function clear() {
|
||||
runtimeState.eventNext = null;
|
||||
runtimeState.publicEventNext = null;
|
||||
|
||||
runtimeState.runtime = {
|
||||
...initialRuntime,
|
||||
// persist session stuff
|
||||
actualStart: runtimeState.runtime.actualStart,
|
||||
numEvents: runtimeState.runtime.numEvents,
|
||||
};
|
||||
runtimeState.runtime.offset = null;
|
||||
runtimeState.runtime.actualStart = null;
|
||||
runtimeState.runtime.expectedEnd = null;
|
||||
runtimeState.runtime.selectedEventIndex = null;
|
||||
|
||||
runtimeState.timer.playback = Playback.Stop;
|
||||
runtimeState.clock = clock.timeNow();
|
||||
@@ -123,7 +121,7 @@ export function updateRundownData(rundownData: RundownData) {
|
||||
|
||||
runtimeState.runtime.numEvents = rundownData.numEvents;
|
||||
runtimeState.runtime.plannedStart = rundownData.firstStart;
|
||||
runtimeState.runtime.plannedEnd = rundownData.firstStart + rundownData.totalDuration;
|
||||
runtimeState.runtime.plannedEnd = rundownData.lastEnd;
|
||||
runtimeState.runtime.expectedEnd = getExpectedEnd(runtimeState);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,10 +19,9 @@ export function ensureDirectory(directory: string): void {
|
||||
/**
|
||||
* Ensures that a filename ends with .json extension
|
||||
*/
|
||||
export function ensureJsonExtension(filename: string): string {
|
||||
export function ensureJsonExtension(filename: string | undefined): string | undefined {
|
||||
if (!filename) return filename;
|
||||
|
||||
return filename.includes('.json') ? filename : `${filename}.json`;
|
||||
return filename.endsWith('.json') ? filename : `${filename}.json`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-2
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"name": "ontime",
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.2",
|
||||
"description": "Time keeping for live events",
|
||||
"keywords": [
|
||||
"lightdev",
|
||||
"ontime",
|
||||
"timer",
|
||||
"rundown"
|
||||
|
||||
@@ -5,7 +5,7 @@ export const MILLIS_PER_MINUTE = 1000 * 60;
|
||||
export const MILLIS_PER_HOUR = 1000 * 60 * 60;
|
||||
|
||||
function convertMillis(millis: MaybeNumber, conversion: number) {
|
||||
if (millis == null || millis === 0) {
|
||||
if (!millis) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
Generated
+210
-65
@@ -146,7 +146,7 @@ importers:
|
||||
version: 13.4.0(react-dom@18.2.0)(react@18.2.0)
|
||||
'@testing-library/user-event':
|
||||
specifier: ^14.1.1
|
||||
version: 14.4.3(@testing-library/dom@9.3.4)
|
||||
version: 14.4.3(@testing-library/dom@10.1.0)
|
||||
'@types/color':
|
||||
specifier: ^3.0.3
|
||||
version: 3.0.3
|
||||
@@ -261,15 +261,12 @@ importers:
|
||||
express:
|
||||
specifier: ^4.18.2
|
||||
version: 4.18.2
|
||||
express-session:
|
||||
specifier: ^1.17.3
|
||||
version: 1.17.3
|
||||
express-static-gzip:
|
||||
specifier: ^2.1.7
|
||||
version: 2.1.7
|
||||
express-validator:
|
||||
specifier: ^6.14.2
|
||||
version: 6.14.2
|
||||
specifier: ^7.0.1
|
||||
version: 7.0.1
|
||||
fast-equals:
|
||||
specifier: ^5.0.1
|
||||
version: 5.0.1
|
||||
@@ -345,7 +342,7 @@ importers:
|
||||
version: 8.53.0
|
||||
eslint-plugin-prettier:
|
||||
specifier: ^5.0.1
|
||||
version: 5.0.1(eslint-config-prettier@9.0.0)(eslint@8.53.0)(prettier@3.0.3)
|
||||
version: 5.0.1(eslint@8.53.0)(prettier@3.0.3)
|
||||
nodemon:
|
||||
specifier: ^2.0.20
|
||||
version: 2.0.20
|
||||
@@ -369,7 +366,7 @@ importers:
|
||||
version: 5.4.3
|
||||
vitest:
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.2(@types/node@18.11.18)(jsdom@21.1.0)(sass@1.57.1)
|
||||
version: 1.2.2(@types/node@18.11.18)
|
||||
|
||||
packages/types:
|
||||
devDependencies:
|
||||
@@ -463,6 +460,14 @@ packages:
|
||||
'@babel/highlight': 7.23.4
|
||||
chalk: 2.4.2
|
||||
|
||||
/@babel/code-frame@7.24.2:
|
||||
resolution: {integrity: sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/highlight': 7.24.5
|
||||
picocolors: 1.0.0
|
||||
dev: true
|
||||
|
||||
/@babel/compat-data@7.23.5:
|
||||
resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -624,6 +629,11 @@ packages:
|
||||
resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
/@babel/helper-validator-identifier@7.24.5:
|
||||
resolution: {integrity: sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: true
|
||||
|
||||
/@babel/helper-validator-option@7.23.5:
|
||||
resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -648,6 +658,16 @@ packages:
|
||||
chalk: 2.4.2
|
||||
js-tokens: 4.0.0
|
||||
|
||||
/@babel/highlight@7.24.5:
|
||||
resolution: {integrity: sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.24.5
|
||||
chalk: 2.4.2
|
||||
js-tokens: 4.0.0
|
||||
picocolors: 1.0.0
|
||||
dev: true
|
||||
|
||||
/@babel/parser@7.23.6:
|
||||
resolution: {integrity: sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
@@ -697,8 +717,8 @@ packages:
|
||||
regenerator-runtime: 0.13.11
|
||||
dev: false
|
||||
|
||||
/@babel/runtime@7.24.0:
|
||||
resolution: {integrity: sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==}
|
||||
/@babel/runtime@7.24.5:
|
||||
resolution: {integrity: sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
regenerator-runtime: 0.14.1
|
||||
@@ -3059,6 +3079,20 @@ packages:
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/@testing-library/dom@10.1.0:
|
||||
resolution: {integrity: sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA==}
|
||||
engines: {node: '>=18'}
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.24.2
|
||||
'@babel/runtime': 7.24.5
|
||||
'@types/aria-query': 5.0.4
|
||||
aria-query: 5.3.0
|
||||
chalk: 4.1.2
|
||||
dom-accessibility-api: 0.5.16
|
||||
lz-string: 1.5.0
|
||||
pretty-format: 27.5.1
|
||||
dev: true
|
||||
|
||||
/@testing-library/dom@8.19.1:
|
||||
resolution: {integrity: sha512-P6iIPyYQ+qH8CvGauAqanhVnjrnRe0IZFSYCeGkSRW9q3u8bdVn2NPI+lasFyVsEQn1J/IFmp5Aax41+dAP9wg==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -3073,20 +3107,6 @@ packages:
|
||||
pretty-format: 27.5.1
|
||||
dev: true
|
||||
|
||||
/@testing-library/dom@9.3.4:
|
||||
resolution: {integrity: sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==}
|
||||
engines: {node: '>=14'}
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.23.5
|
||||
'@babel/runtime': 7.24.0
|
||||
'@types/aria-query': 5.0.4
|
||||
aria-query: 5.1.3
|
||||
chalk: 4.1.2
|
||||
dom-accessibility-api: 0.5.16
|
||||
lz-string: 1.5.0
|
||||
pretty-format: 27.5.1
|
||||
dev: true
|
||||
|
||||
/@testing-library/jest-dom@5.16.5:
|
||||
resolution: {integrity: sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA==}
|
||||
engines: {node: '>=8', npm: '>=6', yarn: '>=1'}
|
||||
@@ -3116,13 +3136,13 @@ packages:
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: true
|
||||
|
||||
/@testing-library/user-event@14.4.3(@testing-library/dom@9.3.4):
|
||||
/@testing-library/user-event@14.4.3(@testing-library/dom@10.1.0):
|
||||
resolution: {integrity: sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q==}
|
||||
engines: {node: '>=12', npm: '>=6'}
|
||||
peerDependencies:
|
||||
'@testing-library/dom': '>=7.21.4'
|
||||
dependencies:
|
||||
'@testing-library/dom': 9.3.4
|
||||
'@testing-library/dom': 10.1.0
|
||||
dev: true
|
||||
|
||||
/@tootallnate/once@2.0.0:
|
||||
@@ -3975,7 +3995,7 @@ packages:
|
||||
lodash.isplainobject: 4.0.6
|
||||
lodash.union: 4.6.0
|
||||
normalize-path: 3.0.0
|
||||
readable-stream: 2.3.7
|
||||
readable-stream: 2.3.8
|
||||
dev: true
|
||||
|
||||
/archiver-utils@3.0.4:
|
||||
@@ -4053,6 +4073,12 @@ packages:
|
||||
deep-equal: 2.2.0
|
||||
dev: true
|
||||
|
||||
/aria-query@5.3.0:
|
||||
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
dev: true
|
||||
|
||||
/array-flatten@1.1.1:
|
||||
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
|
||||
dev: false
|
||||
@@ -4655,11 +4681,6 @@ packages:
|
||||
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
||||
dev: false
|
||||
|
||||
/cookie@0.4.2:
|
||||
resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/cookie@0.5.0:
|
||||
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -4955,6 +4976,11 @@ packages:
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: false
|
||||
|
||||
/dequal@2.0.3:
|
||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/destroy@1.2.0:
|
||||
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
|
||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||
@@ -5426,6 +5452,26 @@ packages:
|
||||
synckit: 0.8.5
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-prettier@5.0.1(eslint@8.53.0)(prettier@3.0.3):
|
||||
resolution: {integrity: sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
'@types/eslint': '>=8.0.0'
|
||||
eslint: '>=8.0.0'
|
||||
eslint-config-prettier: '*'
|
||||
prettier: '>=3.0.0'
|
||||
peerDependenciesMeta:
|
||||
'@types/eslint':
|
||||
optional: true
|
||||
eslint-config-prettier:
|
||||
optional: true
|
||||
dependencies:
|
||||
eslint: 8.53.0
|
||||
prettier: 3.0.3
|
||||
prettier-linter-helpers: 1.0.0
|
||||
synckit: 0.8.5
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-react-hooks@4.6.0(eslint@8.53.0):
|
||||
resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -5682,22 +5728,6 @@ packages:
|
||||
jest-util: 29.3.1
|
||||
dev: true
|
||||
|
||||
/express-session@1.17.3:
|
||||
resolution: {integrity: sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dependencies:
|
||||
cookie: 0.4.2
|
||||
cookie-signature: 1.0.6
|
||||
debug: 2.6.9
|
||||
depd: 2.0.0
|
||||
on-headers: 1.0.2
|
||||
parseurl: 1.3.3
|
||||
safe-buffer: 5.2.1
|
||||
uid-safe: 2.1.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/express-static-gzip@2.1.7:
|
||||
resolution: {integrity: sha512-QOCZUC+lhPPCjIJKpQGu1Oa61Axg9Mq09Qvit8Of7kzpMuwDeMSqjjQteQS3OVw/GkENBoSBheuQDWPlngImvw==}
|
||||
dependencies:
|
||||
@@ -5706,12 +5736,12 @@ packages:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/express-validator@6.14.2:
|
||||
resolution: {integrity: sha512-8XfAUrQ6Y7dIIuy9KcUPCfG/uCbvREctrxf5EeeME+ulanJ4iiW71lWmm9r4YcKKYOCBMan0WpVg7FtHu4Z4Wg==}
|
||||
/express-validator@7.0.1:
|
||||
resolution: {integrity: sha512-oB+z9QOzQIE8FnlINqyIFA8eIckahC6qc8KtqLdLJcU3/phVyuhXH3bA4qzcrhme+1RYaCSwrq+TlZ/kAKIARA==}
|
||||
engines: {node: '>= 8.0.0'}
|
||||
dependencies:
|
||||
lodash: 4.17.21
|
||||
validator: 13.7.0
|
||||
validator: 13.12.0
|
||||
dev: false
|
||||
|
||||
/express@4.18.2:
|
||||
@@ -7577,6 +7607,7 @@ packages:
|
||||
/on-headers@1.0.2:
|
||||
resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: true
|
||||
|
||||
/once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
@@ -7960,11 +7991,6 @@ packages:
|
||||
resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
/random-bytes@1.0.0:
|
||||
resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: false
|
||||
|
||||
/range-parser@1.2.1:
|
||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -8178,6 +8204,18 @@ packages:
|
||||
string_decoder: 1.1.1
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
/readable-stream@2.3.8:
|
||||
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
||||
dependencies:
|
||||
core-util-is: 1.0.3
|
||||
inherits: 2.0.4
|
||||
isarray: 1.0.0
|
||||
process-nextick-args: 2.0.1
|
||||
safe-buffer: 5.1.2
|
||||
string_decoder: 1.1.1
|
||||
util-deprecate: 1.0.2
|
||||
dev: true
|
||||
|
||||
/readable-stream@3.6.2:
|
||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -9179,13 +9217,6 @@ packages:
|
||||
resolution: {integrity: sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==}
|
||||
dev: true
|
||||
|
||||
/uid-safe@2.1.5:
|
||||
resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
random-bytes: 1.0.0
|
||||
dev: false
|
||||
|
||||
/unbox-primitive@1.0.2:
|
||||
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
|
||||
dependencies:
|
||||
@@ -9325,8 +9356,8 @@ packages:
|
||||
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
|
||||
dev: true
|
||||
|
||||
/validator@13.7.0:
|
||||
resolution: {integrity: sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==}
|
||||
/validator@13.12.0:
|
||||
resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==}
|
||||
engines: {node: '>= 0.10'}
|
||||
dev: false
|
||||
|
||||
@@ -9346,6 +9377,27 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/vite-node@1.2.2(@types/node@18.11.18):
|
||||
resolution: {integrity: sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.3.4
|
||||
pathe: 1.1.1
|
||||
picocolors: 1.0.0
|
||||
vite: 5.1.0(@types/node@18.11.18)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
dev: true
|
||||
|
||||
/vite-node@1.2.2(@types/node@18.11.18)(sass@1.57.1):
|
||||
resolution: {integrity: sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
@@ -9408,6 +9460,42 @@ packages:
|
||||
- typescript
|
||||
dev: true
|
||||
|
||||
/vite@5.1.0(@types/node@18.11.18):
|
||||
resolution: {integrity: sha512-STmSFzhY4ljuhz14bg9LkMTk3d98IO6DIArnTY6MeBwiD1Za2StcQtz7fzOUnRCqrHSD5+OS2reg4HOz1eoLnw==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@types/node': ^18.0.0 || >=20.0.0
|
||||
less: '*'
|
||||
lightningcss: ^1.21.0
|
||||
sass: '*'
|
||||
stylus: '*'
|
||||
sugarss: '*'
|
||||
terser: ^5.4.0
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
less:
|
||||
optional: true
|
||||
lightningcss:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
stylus:
|
||||
optional: true
|
||||
sugarss:
|
||||
optional: true
|
||||
terser:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/node': 18.11.18
|
||||
esbuild: 0.19.10
|
||||
postcss: 8.4.35
|
||||
rollup: 4.9.1
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
dev: true
|
||||
|
||||
/vite@5.1.0(@types/node@18.11.18)(sass@1.57.1):
|
||||
resolution: {integrity: sha512-STmSFzhY4ljuhz14bg9LkMTk3d98IO6DIArnTY6MeBwiD1Za2StcQtz7fzOUnRCqrHSD5+OS2reg4HOz1eoLnw==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
@@ -9445,6 +9533,63 @@ packages:
|
||||
fsevents: 2.3.3
|
||||
dev: true
|
||||
|
||||
/vitest@1.2.2(@types/node@18.11.18):
|
||||
resolution: {integrity: sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@edge-runtime/vm': '*'
|
||||
'@types/node': ^18.0.0 || >=20.0.0
|
||||
'@vitest/browser': ^1.0.0
|
||||
'@vitest/ui': ^1.0.0
|
||||
happy-dom: '*'
|
||||
jsdom: '*'
|
||||
peerDependenciesMeta:
|
||||
'@edge-runtime/vm':
|
||||
optional: true
|
||||
'@types/node':
|
||||
optional: true
|
||||
'@vitest/browser':
|
||||
optional: true
|
||||
'@vitest/ui':
|
||||
optional: true
|
||||
happy-dom:
|
||||
optional: true
|
||||
jsdom:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/node': 18.11.18
|
||||
'@vitest/expect': 1.2.2
|
||||
'@vitest/runner': 1.2.2
|
||||
'@vitest/snapshot': 1.2.2
|
||||
'@vitest/spy': 1.2.2
|
||||
'@vitest/utils': 1.2.2
|
||||
acorn-walk: 8.3.2
|
||||
cac: 6.7.14
|
||||
chai: 4.3.10
|
||||
debug: 4.3.4
|
||||
execa: 8.0.1
|
||||
local-pkg: 0.5.0
|
||||
magic-string: 0.30.5
|
||||
pathe: 1.1.1
|
||||
picocolors: 1.0.0
|
||||
std-env: 3.6.0
|
||||
strip-literal: 1.3.0
|
||||
tinybench: 2.5.1
|
||||
tinypool: 0.8.2
|
||||
vite: 5.1.0(@types/node@18.11.18)
|
||||
vite-node: 1.2.2(@types/node@18.11.18)
|
||||
why-is-node-running: 2.2.2
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
dev: true
|
||||
|
||||
/vitest@1.2.2(@types/node@18.11.18)(jsdom@21.1.0)(sass@1.57.1):
|
||||
resolution: {integrity: sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
|
||||
+6
-5
@@ -10,7 +10,7 @@
|
||||
},
|
||||
"settings": {
|
||||
"app": "ontime",
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.0-beta.3",
|
||||
"serverPort": 4001,
|
||||
"editorKey": null,
|
||||
"operatorKey": null,
|
||||
@@ -18,11 +18,12 @@
|
||||
"language": "en"
|
||||
},
|
||||
"viewSettings": {
|
||||
"overrideStyles": false,
|
||||
"normalColor": "#ffffffcc",
|
||||
"warningColor": "#FFAB33",
|
||||
"dangerColor": "#ED3333",
|
||||
"endMessage": ""
|
||||
"endMessage": "",
|
||||
"freezeEnd": false,
|
||||
"normalColor": "#ffffffcc",
|
||||
"overrideStyles": false,
|
||||
"warningColor": "#FFAB33"
|
||||
},
|
||||
"urlPresets": [],
|
||||
"customFields": {},
|
||||
|
||||
Reference in New Issue
Block a user