mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-09 01:13:55 +00:00
feat: optional welcome modal
welcome modal is persisted in app state created endpoints to set the visibility added UI to support feature Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>
This commit is contained in:
@@ -19,3 +19,10 @@ export async function getSettings(): Promise<Settings> {
|
||||
export async function postSettings(data: Settings): Promise<AxiosResponse<Settings>> {
|
||||
return axios.post(settingsPath, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows setting the welcome modal dialog state from the clients
|
||||
*/
|
||||
export async function postShowWelcomeDialog(show: boolean) {
|
||||
axios.post(`${settingsPath}/welcomedialog`, { show });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button, Modal, ModalBody, ModalCloseButton, ModalContent, ModalOverlay } from '@chakra-ui/react';
|
||||
import { Button, Checkbox, Modal, ModalBody, ModalCloseButton, ModalContent, ModalOverlay } from '@chakra-ui/react';
|
||||
|
||||
import { loadDemo, loadProject } from '../../../common/api/db';
|
||||
import { postShowWelcomeDialog } from '../../../common/api/settings';
|
||||
import { invalidateAllCaches } from '../../../common/api/utils';
|
||||
import ExternalLink from '../../../common/components/external-link/ExternalLink';
|
||||
import { appVersion, discordUrl, documentationUrl, websiteUrl } from '../../../externals';
|
||||
@@ -69,7 +70,7 @@ export default function Welcome(props: WelcomeProps) {
|
||||
</div>
|
||||
<div className={style.column}>
|
||||
<div className={style.header}>Welcome to Ontime</div>
|
||||
<Editor.Title>Recent Projects</Editor.Title>
|
||||
<Editor.Title>Select project</Editor.Title>
|
||||
<div className={style.tableContainer}>
|
||||
<table className={style.table}>
|
||||
<thead>
|
||||
@@ -84,7 +85,7 @@ export default function Welcome(props: WelcomeProps) {
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.buttonRow}>
|
||||
<Button size='sm' variant='ontime-ghosted' onClick={handleLoadDemo}>
|
||||
<Button size='sm' variant='ontime-subtle' onClick={handleLoadDemo}>
|
||||
Load demo project
|
||||
</Button>
|
||||
<ImportProjectButton onFinish={handleClose} />
|
||||
@@ -92,6 +93,14 @@ export default function Welcome(props: WelcomeProps) {
|
||||
Create new...
|
||||
</Button>
|
||||
</div>
|
||||
<Checkbox
|
||||
size='sm'
|
||||
variant='ontime-ondark'
|
||||
checked
|
||||
onChange={(event) => postShowWelcomeDialog(event.target.checked)}
|
||||
>
|
||||
Show this modal on next startup
|
||||
</Checkbox>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
@@ -34,7 +34,7 @@ export class SocketServer implements IAdapter {
|
||||
private wss: WebSocketServer | null;
|
||||
private readonly clients: Map<string, Client>;
|
||||
private lastConnection: Date | null = null;
|
||||
private isFirstEditor = true;
|
||||
private shouldShowWelcome = true;
|
||||
|
||||
constructor() {
|
||||
if (instance) {
|
||||
@@ -47,7 +47,8 @@ export class SocketServer implements IAdapter {
|
||||
this.wss = null;
|
||||
}
|
||||
|
||||
init(server: Server, prefix?: string) {
|
||||
init(server: Server, showWelcome: boolean, prefix?: string) {
|
||||
this.shouldShowWelcome = showWelcome;
|
||||
this.wss = new WebSocketServer({ path: `${prefix}/ws`, server, maxPayload: this.MAX_PAYLOAD });
|
||||
|
||||
this.wss.on('connection', (ws) => {
|
||||
@@ -136,8 +137,8 @@ export class SocketServer implements IAdapter {
|
||||
previousData.path = payload;
|
||||
this.clients.set(clientId, previousData);
|
||||
|
||||
if (payload.includes('editor') && this.isFirstEditor) {
|
||||
this.isFirstEditor = false;
|
||||
if (payload.includes('editor') && this.shouldShowWelcome) {
|
||||
this.shouldShowWelcome = false;
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'dialog',
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { Request, Response } from 'express';
|
||||
import { isDocker } from '../../externals.js';
|
||||
import { failEmptyObjects } from '../../utils/routerUtils.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';
|
||||
|
||||
@@ -65,3 +66,8 @@ export async function postSettings(req: Request, res: Response<Settings | ErrorR
|
||||
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,8 +1,10 @@
|
||||
import express from 'express';
|
||||
import { getSettings, postSettings } from './settings.controller.js';
|
||||
import { validateSettings } from './settings.validation.js';
|
||||
import { getSettings, postSettings, postWelcomeDialog } from './settings.controller.js';
|
||||
import { validateSettings, validateWelcomeDialog } from './settings.validation.js';
|
||||
|
||||
export const router = express.Router();
|
||||
|
||||
router.post('/welcomedialog', validateWelcomeDialog, postWelcomeDialog);
|
||||
|
||||
router.get('/', getSettings);
|
||||
router.post('/', validateSettings, postSettings);
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
import { body, validationResult } from 'express-validator';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
/**
|
||||
* @description Validates object for POST /ontime/settings/welcomedialog
|
||||
*/
|
||||
export const validateWelcomeDialog = [
|
||||
body('show').exists().isBoolean(),
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* @description Validates object for POST /ontime/settings
|
||||
*/
|
||||
|
||||
@@ -44,6 +44,7 @@ import { clearUploadfolder } from './utils/upload.js';
|
||||
import { generateCrashReport } from './utils/generateCrashReport.js';
|
||||
import { timerConfig } from './config/config.js';
|
||||
import { serverTryDesiredPort, getNetworkInterfaces } from './utils/network.js';
|
||||
import { getShowWelcomeDialog } from './services/app-state-service/AppStateService.js';
|
||||
|
||||
console.log('\n');
|
||||
consoleHighlight(`Starting Ontime version ${ONTIME_VERSION}`);
|
||||
@@ -166,8 +167,9 @@ export const startServer = async (
|
||||
// the express server must be started before the socket otherwise the on error event listener will not attach properly
|
||||
const resultPort = await serverTryDesiredPort(expressServer, desiredPort);
|
||||
await getDataProvider().setSettings({ ...settings, serverPort: resultPort });
|
||||
const showWelcome = await getShowWelcomeDialog();
|
||||
|
||||
socket.init(expressServer, prefix);
|
||||
socket.init(expressServer, showWelcome, prefix);
|
||||
|
||||
/**
|
||||
* Module initialises the services and provides initial payload for the store
|
||||
|
||||
@@ -8,6 +8,7 @@ import { shouldCrashDev } from '../../utils/development.js';
|
||||
|
||||
interface AppState {
|
||||
lastLoadedProject?: string;
|
||||
showWelcomeDialog?: boolean;
|
||||
}
|
||||
|
||||
const adapter = new JSONFile<AppState>(publicFiles.appState);
|
||||
@@ -34,3 +35,19 @@ export async function setLastLoadedProject(filename: string): Promise<void> {
|
||||
config.data.lastLoadedProject = filename;
|
||||
await config.write();
|
||||
}
|
||||
|
||||
export async function getShowWelcomeDialog(): Promise<boolean> {
|
||||
// in test environment, we do not want the dialog
|
||||
if (isTest) return false;
|
||||
|
||||
await config.read();
|
||||
return config.data.showWelcomeDialog ?? true; // default to true
|
||||
}
|
||||
|
||||
export async function setShowWelcomeDialog(show: boolean): Promise<boolean> {
|
||||
if (isTest) return;
|
||||
|
||||
config.data.showWelcomeDialog = show;
|
||||
await config.write();
|
||||
return show;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,6 @@ const fileToDownload = 'e2e/tests/fixtures/tmp/test-db.json';
|
||||
test('project file upload', async ({ page }) => {
|
||||
await page.goto('http://localhost:4001/editor');
|
||||
|
||||
// close the welcome modal if it is open
|
||||
await page.keyboard.down('Escape');
|
||||
|
||||
await page.getByRole('button', { name: 'Edit' }).click();
|
||||
await page.getByRole('button', { name: 'Clear rundown' }).click();
|
||||
await page.getByRole('button', { name: 'Delete all' }).click();
|
||||
@@ -41,9 +38,6 @@ test('project file upload', async ({ page }) => {
|
||||
test('project file download', async ({ page }) => {
|
||||
await page.goto('http://localhost:4001/editor');
|
||||
|
||||
// close the welcome modal if it is open
|
||||
await page.keyboard.down('Escape');
|
||||
|
||||
await page.getByRole('button', { name: 'toggle settings' }).click();
|
||||
await page.getByRole('button', { name: 'Project', exact: true }).click();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user