mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-25 08:59:13 +00:00
feat: show welcome screen on first login
This commit is contained in:
committed by
Carlos Valente
parent
7bf90d669a
commit
22fe65c655
@@ -0,0 +1,24 @@
|
|||||||
|
import { MaybeString } from 'ontime-types';
|
||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
interface DialogStore {
|
||||||
|
showDialog: MaybeString;
|
||||||
|
setDialog: (name: string) => void;
|
||||||
|
clearDialog: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to allow the server to show a dialog in the client
|
||||||
|
*/
|
||||||
|
export const useDialogStore = create<DialogStore>((set) => ({
|
||||||
|
showDialog: null,
|
||||||
|
clearDialog: () => set({ showDialog: null }),
|
||||||
|
setDialog: (name) => set({ showDialog: name }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows setting a dialog to show (outside react)
|
||||||
|
*/
|
||||||
|
export function addDialog(name: string): void {
|
||||||
|
useDialogStore.getState().setDialog(name);
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
setClientRedirect,
|
setClientRedirect,
|
||||||
setClients,
|
setClients,
|
||||||
} from '../stores/clientStore';
|
} from '../stores/clientStore';
|
||||||
|
import { addDialog } from '../stores/dialogStore';
|
||||||
import { addLog } from '../stores/logger';
|
import { addLog } from '../stores/logger';
|
||||||
import { patchRuntime, runtimeStore } from '../stores/runtime';
|
import { patchRuntime, runtimeStore } from '../stores/runtime';
|
||||||
|
|
||||||
@@ -118,6 +119,13 @@ export const connectSocket = () => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'dialog': {
|
||||||
|
if (payload.dialog === 'welcome') {
|
||||||
|
addDialog('welcome');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'ontime-log': {
|
case 'ontime-log': {
|
||||||
addLog(payload as Log);
|
addLog(payload as Log);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import useAppSettingsNavigation from '../app-settings/useAppSettingsNavigation';
|
|||||||
import { EditorOverview } from '../overview/Overview';
|
import { EditorOverview } from '../overview/Overview';
|
||||||
|
|
||||||
import Finder from './finder/Finder';
|
import Finder from './finder/Finder';
|
||||||
|
import WelcomePlacement from './welcome/WelcomePlacement';
|
||||||
|
|
||||||
import styles from './Editor.module.scss';
|
import styles from './Editor.module.scss';
|
||||||
|
|
||||||
@@ -55,6 +56,7 @@ export default function Editor() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.mainContainer} data-testid='event-editor'>
|
<div className={styles.mainContainer} data-testid='event-editor'>
|
||||||
|
<WelcomePlacement />
|
||||||
<Finder isOpen={isFinderOpen} onClose={onFinderClose} />
|
<Finder isOpen={isFinderOpen} onClose={onFinderClose} />
|
||||||
<ProductionNavigationMenu isMenuOpen={isMenuOpen} onMenuClose={onClose} />
|
<ProductionNavigationMenu isMenuOpen={isMenuOpen} onMenuClose={onClose} />
|
||||||
<EditorOverview>
|
<EditorOverview>
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
.sections {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 5fr;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
max-width: 100px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonRow {
|
||||||
|
margin-top: 1rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableContainer {
|
||||||
|
max-height: 350px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
tbody {
|
||||||
|
background-color: $gray-1300;
|
||||||
|
tr {
|
||||||
|
&:has(:hover) {
|
||||||
|
background-color: $gray-1350;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding-inline: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
font-size: calc(1rem - 3px);
|
||||||
|
color: $label-gray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.current {
|
||||||
|
background-color: $blue-700;
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Button, Modal, ModalBody, ModalCloseButton, ModalContent, ModalOverlay } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import { loadDemo, loadProject } from '../../../common/api/db';
|
||||||
|
import { invalidateAllCaches } from '../../../common/api/utils';
|
||||||
|
import ExternalLink from '../../../common/components/external-link/ExternalLink';
|
||||||
|
import { appVersion, discordUrl, documentationUrl, websiteUrl } from '../../../externals';
|
||||||
|
|
||||||
|
import ImportProjectButton from './composite/ImportProjectButton';
|
||||||
|
import WelcomeProjectList from './composite/WelcomeProjectList';
|
||||||
|
|
||||||
|
import style from './Welcome.module.scss';
|
||||||
|
|
||||||
|
interface WelcomeProps {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Welcome(props: WelcomeProps) {
|
||||||
|
const { onClose } = props;
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
/** handle cleanup actions before request closing the modal */
|
||||||
|
const handleClose = () => {
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** handle loading a selected project */
|
||||||
|
const handleLoadProject = async (filename: string) => {
|
||||||
|
try {
|
||||||
|
await loadProject(filename);
|
||||||
|
await invalidateAllCaches();
|
||||||
|
handleClose();
|
||||||
|
} catch (_error) {
|
||||||
|
/** no error handling for now */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** handle loading the demo project */
|
||||||
|
const handleLoadDemo = async () => {
|
||||||
|
try {
|
||||||
|
await loadDemo();
|
||||||
|
await invalidateAllCaches();
|
||||||
|
handleClose();
|
||||||
|
} catch (_error) {
|
||||||
|
/** no error handling for now */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** handle redirect to create modal */
|
||||||
|
const handleCallCreate = () => {
|
||||||
|
navigate('/editor?settings=project__create');
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen onClose={handleClose} variant='ontime'>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent maxWidth='max(640px, 40vw)'>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody>
|
||||||
|
<div className={style.sections}>
|
||||||
|
<div className={style.column}>
|
||||||
|
<img src='ontime-logo.png' alt='ontime' className={style.logo} />
|
||||||
|
<div>Ontime v{appVersion}</div>
|
||||||
|
<ExternalLink href={websiteUrl}>Website</ExternalLink>
|
||||||
|
<ExternalLink href={documentationUrl}>Read the docs</ExternalLink>
|
||||||
|
<ExternalLink href={discordUrl}>Discord server</ExternalLink>
|
||||||
|
</div>
|
||||||
|
<div className={style.column}>
|
||||||
|
<div className={style.header}>Welcome to Ontime</div>
|
||||||
|
<div className={style.tableContainer}>
|
||||||
|
<table className={style.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Project Name</th>
|
||||||
|
<th>Last Used</th>
|
||||||
|
<th />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<WelcomeProjectList loadProject={handleLoadProject} onClose={handleClose} />
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={style.buttonRow}>
|
||||||
|
<ImportProjectButton onFinish={handleClose} />
|
||||||
|
<Button size='sm' variant='ontime-subtle' onClick={handleLoadDemo}>
|
||||||
|
Load demo project
|
||||||
|
</Button>
|
||||||
|
<Button size='sm' variant='ontime-filled' onClick={handleCallCreate}>
|
||||||
|
Create new...
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { useDialogStore } from '../../../common/stores/dialogStore';
|
||||||
|
|
||||||
|
import Welcome from './Welcome';
|
||||||
|
|
||||||
|
export default function WelcomePlacement() {
|
||||||
|
const showDialog = useDialogStore((state) => state.showDialog);
|
||||||
|
const clearDialog = useDialogStore((state) => state.clearDialog);
|
||||||
|
|
||||||
|
if (!showDialog) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Welcome onClose={clearDialog} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* Handles importing of a project in the welcome modal
|
||||||
|
* the logic is mostly duplicated from ManageProjects.tsx
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ChangeEvent, useRef } from 'react';
|
||||||
|
import { Button, Input } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import { uploadProjectFile } from '../../../../common/api/db';
|
||||||
|
import { invalidateAllCaches } from '../../../../common/api/utils';
|
||||||
|
import { validateProjectFile } from '../../../../common/utils/uploadUtils';
|
||||||
|
|
||||||
|
interface ImportProjectButtonProps {
|
||||||
|
onFinish: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ImportProjectButton(props: ImportProjectButtonProps) {
|
||||||
|
const { onFinish } = props;
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleSelectFile = () => {
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImport = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const selectedFile = event.target?.files?.[0];
|
||||||
|
if (!selectedFile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
validateProjectFile(selectedFile);
|
||||||
|
await uploadProjectFile(selectedFile);
|
||||||
|
} catch (error) {
|
||||||
|
/** we do not handle errors here */
|
||||||
|
} finally {
|
||||||
|
await invalidateAllCaches();
|
||||||
|
onFinish();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
ref={fileInputRef}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
type='file'
|
||||||
|
onChange={handleImport}
|
||||||
|
accept='.json'
|
||||||
|
data-testid='file-input'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button size='sm' variant='ontime-subtle' onClick={handleSelectFile}>
|
||||||
|
Import project
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Button } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import { useOrderedProjectList } from '../../../../common/hooks-query/useProjectList';
|
||||||
|
|
||||||
|
import style from '../Welcome.module.scss';
|
||||||
|
|
||||||
|
interface WelcomeProjectListProps {
|
||||||
|
loadProject: (filename: string) => Promise<void>;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WelcomeProjectList(props: WelcomeProjectListProps) {
|
||||||
|
const { loadProject, onClose } = props;
|
||||||
|
const { data } = useOrderedProjectList();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tbody>
|
||||||
|
{data.reorderedProjectFiles.map((project) => {
|
||||||
|
if (project.filename === data.lastLoadedProject) {
|
||||||
|
return (
|
||||||
|
<tr className={style.current} key={project.filename}>
|
||||||
|
<td>{project.filename}</td>
|
||||||
|
<td>Loaded from last session</td>
|
||||||
|
<td>
|
||||||
|
<Button variant='ontime-subtle' size='sm' onClick={onClose}>
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<tr key={project.filename}>
|
||||||
|
<td>{project.filename}</td>
|
||||||
|
<td>{new Date(project.updatedAt).toLocaleString()}</td>
|
||||||
|
<td>
|
||||||
|
<Button variant='ontime-subtle' size='sm' onClick={() => loadProject(project.filename)}>
|
||||||
|
Load
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@ export class SocketServer implements IAdapter {
|
|||||||
private wss: WebSocketServer | null;
|
private wss: WebSocketServer | null;
|
||||||
private readonly clients: Map<string, Client>;
|
private readonly clients: Map<string, Client>;
|
||||||
private lastConnection: Date | null = null;
|
private lastConnection: Date | null = null;
|
||||||
|
private isFirstEditor = true;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (instance) {
|
if (instance) {
|
||||||
@@ -134,7 +135,18 @@ export class SocketServer implements IAdapter {
|
|||||||
const previousData = this.clients.get(clientId);
|
const previousData = this.clients.get(clientId);
|
||||||
previousData.path = payload;
|
previousData.path = payload;
|
||||||
this.clients.set(clientId, previousData);
|
this.clients.set(clientId, previousData);
|
||||||
|
|
||||||
|
if (payload.includes('editor') && this.isFirstEditor) {
|
||||||
|
this.isFirstEditor = false;
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'dialog',
|
||||||
|
payload: { dialog: 'welcome' },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sendClientList();
|
this.sendClientList();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user