Compare commits

..

4 Commits

Author SHA1 Message Date
Carlos Valente 89922fe394 revert: fixed size for editor 2025-09-03 17:48:45 +02:00
Alex Christoffer Rasmussen f219ebb8f8 refactor: settings route (#1753)
* refactor: settings route
2025-09-03 17:48:45 +02:00
Carlos Valente eace97ec7a bump version to 4.0.0-alpha.6 2025-09-03 17:48:45 +02:00
Carlos Valente ec1df8b958 fix: bundle translations 2025-09-03 17:48:45 +02:00
18 changed files with 96 additions and 187 deletions
+4
View File
@@ -34,6 +34,10 @@ e2e/tests/fixtures/tmp/*
build/
dist/
# bundled assets
translations.json
override.css
# working stuff
**/TODO.md
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getontime/cli",
"version": "4.0.0-alpha.5",
"version": "4.0.0-alpha.6",
"author": "Carlos Valente",
"description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ontime-ui",
"version": "4.0.0-alpha.5",
"version": "4.0.0-alpha.6",
"private": true,
"type": "module",
"dependencies": {
@@ -1,7 +1,6 @@
import { useQuery } from '@tanstack/react-query';
import { unobfuscate } from 'ontime-utils';
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { APP_SETTINGS } from '../api/constants';
import { getSettings } from '../api/settings';
import { ontimePlaceholderSettings } from '../models/OntimeSettings';
@@ -11,10 +10,6 @@ export default function useSettings() {
queryKey: APP_SETTINGS,
queryFn: getSettings,
placeholderData: (previousData, _previousQuery) => previousData,
retry: 5,
retryDelay: (attempt) => attempt * 2500,
refetchInterval: queryRefetchIntervalSlow,
networkMode: 'always',
select: (data) => {
const unobfuscated = { ...data };
if (data.editorKey) {
+4
View File
@@ -11,6 +11,7 @@ import {
import { isProduction, websocketUrl } from '../../externals';
import {
APP_SETTINGS,
CLIENT_LIST,
CUSTOM_FIELDS,
PROJECT_DATA,
@@ -177,6 +178,9 @@ export const connectSocket = () => {
case RefetchKey.Translation:
ontimeQueryClient.invalidateQueries({ queryKey: TRANSLATION });
break;
case RefetchKey.Settings:
ontimeQueryClient.invalidateQueries({ queryKey: APP_SETTINGS });
break;
default: {
target satisfies never;
break;
@@ -8,7 +8,7 @@
.rundownEditor {
// width is locked to swatch picker elements
width: calc(15 * 2rem + 13 * 0.5rem);
min-width: calc(15 * 2rem + 13 * 0.5rem);
// we dont want a scrollbar when in the modal
.content {
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "ontime-electron",
"version": "4.0.0-alpha.5",
"version": "4.0.0-alpha.6",
"author": "Carlos Valente",
"description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime",
@@ -121,7 +121,8 @@
"from": "../server/src/user/",
"to": "extraResources/user/",
"filter": [
"**/*"
"**/*",
"*{.ts}"
]
}
]
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "ontime-server",
"type": "module",
"main": "src/index.ts",
"version": "4.0.0-alpha.5",
"version": "4.0.0-alpha.6",
"exports": "./src/index.js",
"dependencies": {
"@googleapis/sheets": "^5.0.5",
+2 -6
View File
@@ -1,8 +1,8 @@
import { existsSync } from 'fs';
import { writeFile } from 'node:fs/promises';
import { defaultCss } from '../src/user/styles/bundledCss';
import path from 'path';
import { defaultCss } from '../src/user/styles/bundledCss';
/**
* Script to write contents of bundledCss to override.css
*/
@@ -11,10 +11,6 @@ async function bundleCss() {
const stylesDir = path.resolve(process.cwd(), 'src', 'user', 'styles');
const cssFile = path.resolve(stylesDir, 'override.css');
if (!existsSync(cssFile)) {
throw new Error('File does not exist');
}
await writeFile(cssFile, defaultCss, { encoding: 'utf8' });
} catch (error) {
console.error('Failed writing to CSS file: ', error);
+2 -6
View File
@@ -1,8 +1,8 @@
import { existsSync } from 'fs';
import { writeFile } from 'node:fs/promises';
import { defaultTranslation } from '../src/user/translations/bundledTranslation';
import path from 'path';
import { defaultTranslation } from '../src/user/translations/bundledTranslations.js';
/**
* Script to write contents of default translation to translation.json
*/
@@ -11,10 +11,6 @@ async function bundleTranslation() {
const translationDir = path.resolve(process.cwd(), 'src', 'user', 'translations');
const translationsFile = path.resolve(translationDir, 'translations.json');
if (!existsSync(translationsFile)) {
throw new Error('File does not exist');
}
await writeFile(translationsFile, defaultTranslation, { encoding: 'utf8' });
} catch (error) {
console.error('Failed writing to translations file: ', error);
@@ -1,71 +0,0 @@
import { ErrorResponse, Settings } from 'ontime-types';
import { getErrorMessage, obfuscate } from 'ontime-utils';
import type { Request, Response } from 'express';
import { isDocker } from '../../setup/environment.js';
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
import * as appState from '../../services/app-state-service/AppStateService.js';
import { extractPin } from './settings.utils.js';
export async function getSettings(_req: Request, res: Response<Settings>) {
const settings = getDataProvider().getSettings();
const obfuscatedSettings = { ...settings };
if (settings.editorKey) {
obfuscatedSettings.editorKey = obfuscate(settings.editorKey);
}
if (settings.operatorKey) {
obfuscatedSettings.operatorKey = obfuscate(settings.operatorKey);
}
res.status(200).send(obfuscatedSettings);
}
export async function postSettings(req: Request, res: Response<Settings | ErrorResponse>) {
try {
const settings = getDataProvider().getSettings();
const editorKey = extractPin(req.body?.editorKey, settings.editorKey);
const operatorKey = extractPin(req.body?.operatorKey, settings.operatorKey);
const serverPort = Number(req.body?.serverPort);
//TODO: should this not be part of the validator?
if (isNaN(serverPort)) {
res.status(400).send({ message: `Invalid value found for server port: ${req.body?.serverPort}` });
return;
}
const hasChangedPort = settings.serverPort !== serverPort;
if (isDocker && hasChangedPort) {
res.status(403).json({ message: 'Can`t change port when running inside docker' });
return;
}
let timeFormat = settings.timeFormat;
if (req.body?.timeFormat === '12' || req.body?.timeFormat === '24') {
timeFormat = req.body.timeFormat;
}
const language = req.body?.language || 'en';
const newData = {
...settings,
editorKey,
operatorKey,
timeFormat,
language,
serverPort,
};
await getDataProvider().setSettings(newData);
res.status(200).send(newData);
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
}
export async function postWelcomeDialog(req: Request, res: Response) {
const show = await appState.setShowWelcomeDialog(req.body.show);
res.status(200).send({ show });
}
@@ -1,10 +1,58 @@
import express from 'express';
import { getSettings, postSettings, postWelcomeDialog } from './settings.controller.js';
import { matchedData } from 'express-validator';
import type { Request, Response } from 'express';
import { deepEqual } from 'fast-equals';
import { ErrorResponse, RefetchKey, Settings } from 'ontime-types';
import { getErrorMessage, obfuscate } from 'ontime-utils';
import { validateSettings, validateWelcomeDialog } from './settings.validation.js';
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
import * as appState from '../../services/app-state-service/AppStateService.js';
import { isDocker } from '../../setup/environment.js';
import { sendRefetch } from '../../adapters/WebsocketAdapter.js';
export const router = express.Router();
router.post('/welcomedialog', validateWelcomeDialog, postWelcomeDialog);
router.post('/welcomedialog', validateWelcomeDialog, async (req: Request, res: Response) => {
const show = await appState.setShowWelcomeDialog(req.body.show);
res.status(200).json({ show });
});
router.get('/', getSettings);
router.post('/', validateSettings, postSettings);
router.get('/', (_req: Request, res: Response<Settings>) => {
const settings = getDataProvider().getSettings();
const obfuscatedSettings = { ...settings };
if (settings.editorKey) {
obfuscatedSettings.editorKey = obfuscate(settings.editorKey);
}
if (settings.operatorKey) {
obfuscatedSettings.operatorKey = obfuscate(settings.operatorKey);
}
res.status(200).json(obfuscatedSettings);
});
router.post('/', validateSettings, async (req: Request, res: Response<Settings | ErrorResponse>) => {
try {
const data = matchedData<Settings>(req);
const settings = getDataProvider().getSettings();
if (isDocker && settings.serverPort !== data.serverPort) {
res.status(403).json({ message: 'Can`t change port when running inside docker' });
return;
}
data.version = settings.version;
if (!deepEqual(data, settings)) {
await getDataProvider().setSettings(data);
sendRefetch(RefetchKey.Settings);
}
res.status(200).json(data);
} catch (error) {
const message = getErrorMessage(error);
res.status(400).json({ message });
}
});
@@ -1,15 +0,0 @@
/**
* Business logic for resolving a string
*/
export function extractPin(value: string | undefined | null, fallback: string | null): string | null {
if (value === null) {
return value;
}
if (typeof value === 'undefined') {
return fallback;
}
if (value.length === 0) {
return null;
}
return value;
}
@@ -1,4 +1,4 @@
import { body } from 'express-validator';
import { body, checkExact } from 'express-validator';
import { requestValidationFunction } from '../validation-utils/validationFunction.js';
/**
@@ -6,16 +6,30 @@ import { requestValidationFunction } from '../validation-utils/validationFunctio
*/
export const validateWelcomeDialog = [body('show').isBoolean(), requestValidationFunction];
const pinValidator = (key: string) => {
return body(key)
.isString()
.trim()
.isLength({ min: 0, max: 4 })
.customSanitizer((input) => {
if (input.length === 0) {
return null;
}
return input;
});
};
/**
* @description Validates object for POST /ontime/settings
*/
export const validateSettings = [
body().notEmpty().withMessage('No object found in request'),
body('editorKey').isString().isLength({ min: 0, max: 4 }).optional({ nullable: true }),
body('operatorKey').isString().isLength({ min: 0, max: 4 }).optional({ nullable: true }),
body('timeFormat').isString().isIn(['12', '24']),
body('language').isString(),
body('serverPort').isPort().optional(),
pinValidator('editorKey'),
pinValidator('operatorKey'),
body('timeFormat').isString().isIn(['12', '24']).withMessage('Time format can only be "12" or "24"'),
body('language').isString().trim().notEmpty(),
body('serverPort').isPort().withMessage('Invalid value found for server port').toInt(),
checkExact(),
requestValidationFunction,
];
// TODO: dont allow other keys
+2
View File
@@ -53,6 +53,8 @@ export function getAppDataPath(): string {
* │ │ ├─ override.css
* │ ├─ logo/
* │ │ ├─ logo.png
* │ ├─ translations/
* │ │ ├─ translations.json
*/
/** resolve file URL in both CJS and ESM (build and dev) */
-66
View File
@@ -1,66 +0,0 @@
/**
* This CSS file allows user customisation of the UI
* We expose some CSS properties to facilitate this (see below in :root)
* In the cases where this is missing, you can add your selectors here
*/
:root {
/** Background colour for the views */
--background-color-override: #ececec;
/** Main text colour for the views */
--color-override: #101010;
/** Text colour for the views */
--secondary-color-override: #404040;
/** Accent text colour, used on active elements */
--accent-color-override: #fa5656;
/** Label text colour, used on active elements */
--label-color-override: #6c6c6c;
/** Timer text colour */
--timer-color-override: #202020;
--timer-warning-color-override: #ffbc56;
--timer-danger-color-override: #e69000;
--timer-overtime-color-override: #fa5656;
--timer-pending-color-override: #578AF4;
/** Background for card elements on background */
--card-background-color-override: #fff;
/** Font used for all text in views */
--font-family-override: 'Open Sans';
/** Colour used for external message and aux timer in /timer */
--external-color-override: #161616;
/** View specific features: /backstage */
/** ---- Background highlight for blink behaviour */
--card-background-color-blink-override: #339e4e;
/** ---- Colour used for progress bar background */
--timer-progress-bg-override: #fff;
/** ---- Colour used for progress bar progress */
--timer-progress-override: #202020;
/** View specific features: /op */
--operator-customfield-font-size-override: 1.25rem;
--operator-running-bg-override: #339e4e;
/** View specific features: /studio */
--studio-active: #101010;
--studio-idle: #cfcfcf;
--studio-active-label: #101010;
--studio-idle-label: #595959;
--studio-overtime: #101010;
}
/**
* You can inspect the page in your browser and add the selectors here.
* In the below example, we change the colour of the overlay message in the stage-timer view.
*/
.stage-timer > .message-overlay--active > div {
color: red;
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ontime",
"version": "4.0.0-alpha.5",
"version": "4.0.0-alpha.6",
"description": "Time keeping for live events",
"keywords": [
"ontime",
@@ -7,4 +7,5 @@ export enum RefetchKey {
UrlPresets = 'url-presets',
ViewSettings = 'view-settings',
Translation = 'translation',
Settings = 'settings',
}