From ce8d5349539dffe6f0f01644f57a23cb30f12a9f Mon Sep 17 00:00:00 2001 From: arc-alex Date: Sun, 12 Jan 2025 15:30:22 +0100 Subject: [PATCH] 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> --- apps/client/src/common/api/settings.ts | 7 +++++++ .../src/features/editors/welcome/Welcome.tsx | 15 ++++++++++++--- apps/server/src/adapters/WebsocketAdapter.ts | 9 +++++---- .../api-data/settings/settings.controller.ts | 6 ++++++ .../src/api-data/settings/settings.router.ts | 6 ++++-- .../api-data/settings/settings.validation.ts | 12 ++++++++++++ apps/server/src/app.ts | 4 +++- .../app-state-service/AppStateService.ts | 17 +++++++++++++++++ e2e/tests/000-upload-showfile.spec.ts | 6 ------ 9 files changed, 66 insertions(+), 16 deletions(-) diff --git a/apps/client/src/common/api/settings.ts b/apps/client/src/common/api/settings.ts index a4a9aa1c2..29b6ab469 100644 --- a/apps/client/src/common/api/settings.ts +++ b/apps/client/src/common/api/settings.ts @@ -19,3 +19,10 @@ export async function getSettings(): Promise { export async function postSettings(data: Settings): Promise> { 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 }); +} diff --git a/apps/client/src/features/editors/welcome/Welcome.tsx b/apps/client/src/features/editors/welcome/Welcome.tsx index 3053584a6..b5abad95c 100644 --- a/apps/client/src/features/editors/welcome/Welcome.tsx +++ b/apps/client/src/features/editors/welcome/Welcome.tsx @@ -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) {
Welcome to Ontime
- Recent Projects + Select project
@@ -84,7 +85,7 @@ export default function Welcome(props: WelcomeProps) {
- @@ -92,6 +93,14 @@ export default function Welcome(props: WelcomeProps) { Create new...
+ postShowWelcomeDialog(event.target.checked)} + > + Show this modal on next startup + diff --git a/apps/server/src/adapters/WebsocketAdapter.ts b/apps/server/src/adapters/WebsocketAdapter.ts index cc87be0de..9fc1a6c75 100644 --- a/apps/server/src/adapters/WebsocketAdapter.ts +++ b/apps/server/src/adapters/WebsocketAdapter.ts @@ -34,7 +34,7 @@ export class SocketServer implements IAdapter { private wss: WebSocketServer | null; private readonly clients: Map; 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', diff --git a/apps/server/src/api-data/settings/settings.controller.ts b/apps/server/src/api-data/settings/settings.controller.ts index 7880f632b..c46ac6a3d 100644 --- a/apps/server/src/api-data/settings/settings.controller.ts +++ b/apps/server/src/api-data/settings/settings.controller.ts @@ -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 { + const errors = validationResult(req); + if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); + next(); + }, +]; + /** * @description Validates object for POST /ontime/settings */ diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index eb6b807ca..18afb200a 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -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 diff --git a/apps/server/src/services/app-state-service/AppStateService.ts b/apps/server/src/services/app-state-service/AppStateService.ts index aa0b5fdb2..1f2430099 100644 --- a/apps/server/src/services/app-state-service/AppStateService.ts +++ b/apps/server/src/services/app-state-service/AppStateService.ts @@ -8,6 +8,7 @@ import { shouldCrashDev } from '../../utils/development.js'; interface AppState { lastLoadedProject?: string; + showWelcomeDialog?: boolean; } const adapter = new JSONFile(publicFiles.appState); @@ -34,3 +35,19 @@ export async function setLastLoadedProject(filename: string): Promise { config.data.lastLoadedProject = filename; await config.write(); } + +export async function getShowWelcomeDialog(): Promise { + // 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 { + if (isTest) return; + + config.data.showWelcomeDialog = show; + await config.write(); + return show; +} diff --git a/e2e/tests/000-upload-showfile.spec.ts b/e2e/tests/000-upload-showfile.spec.ts index b83897e9e..28b363b89 100644 --- a/e2e/tests/000-upload-showfile.spec.ts +++ b/e2e/tests/000-upload-showfile.spec.ts @@ -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();