mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-20 14:39:06 +00:00
feat: create project list (#671)
This commit is contained in:
@@ -1,14 +1,15 @@
|
|||||||
// REST stuff
|
// REST stuff
|
||||||
export const PROJECT_DATA = ['project'];
|
|
||||||
export const ALIASES = ['aliases'];
|
export const ALIASES = ['aliases'];
|
||||||
export const USERFIELDS = ['userFields'];
|
|
||||||
export const RUNDOWN = ['rundown'];
|
|
||||||
export const APP_INFO = ['appinfo'];
|
export const APP_INFO = ['appinfo'];
|
||||||
export const OSC_SETTINGS = ['oscSettings'];
|
|
||||||
export const HTTP_SETTINGS = ['httpSettings'];
|
|
||||||
export const APP_SETTINGS = ['appSettings'];
|
export const APP_SETTINGS = ['appSettings'];
|
||||||
export const VIEW_SETTINGS = ['viewSettings'];
|
export const HTTP_SETTINGS = ['httpSettings'];
|
||||||
|
export const OSC_SETTINGS = ['oscSettings'];
|
||||||
|
export const PROJECT_DATA = ['project'];
|
||||||
|
export const PROJECT_LIST = ['projectList'];
|
||||||
|
export const RUNDOWN = ['rundown'];
|
||||||
export const RUNTIME = ['runtimeStore'];
|
export const RUNTIME = ['runtimeStore'];
|
||||||
|
export const USERFIELDS = ['userFields'];
|
||||||
|
export const VIEW_SETTINGS = ['viewSettings'];
|
||||||
|
|
||||||
const location = window.location;
|
const location = window.location;
|
||||||
const socketProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
|
const socketProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
OSCSettings,
|
OSCSettings,
|
||||||
OscSubscription,
|
OscSubscription,
|
||||||
ProjectData,
|
ProjectData,
|
||||||
|
ProjectFileListResponse,
|
||||||
Settings,
|
Settings,
|
||||||
UserFields,
|
UserFields,
|
||||||
ViewSettings,
|
ViewSettings,
|
||||||
@@ -242,6 +243,17 @@ export async function getLatestVersion(): Promise<HasUpdate> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description HTTP POST request to create a new project file with given project data
|
||||||
|
*/
|
||||||
export async function postNew(initialData: Partial<ProjectData>) {
|
export async function postNew(initialData: Partial<ProjectData>) {
|
||||||
return axios.post(`${ontimeURL}/new`, initialData);
|
return axios.post(`${ontimeURL}/new`, initialData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description HTTP request to get the list of available project files
|
||||||
|
*/
|
||||||
|
export async function getProjects(): Promise<ProjectFileListResponse> {
|
||||||
|
const res = await axios.get(`${ontimeURL}/projects`);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { KeyboardEvent, memo, useEffect, useRef, useState } from 'react';
|
import { memo, useEffect, useRef, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { Link, useLocation, useSearchParams } from 'react-router-dom';
|
import { Link, useLocation, useSearchParams } from 'react-router-dom';
|
||||||
import { useDisclosure } from '@chakra-ui/react';
|
import { useDisclosure } from '@chakra-ui/react';
|
||||||
@@ -13,6 +13,7 @@ import { navigatorConstants } from '../../../viewerConfig';
|
|||||||
import useClickOutside from '../../hooks/useClickOutside';
|
import useClickOutside from '../../hooks/useClickOutside';
|
||||||
import useFullscreen from '../../hooks/useFullscreen';
|
import useFullscreen from '../../hooks/useFullscreen';
|
||||||
import { useViewOptionsStore } from '../../stores/viewOptions';
|
import { useViewOptionsStore } from '../../stores/viewOptions';
|
||||||
|
import { isKeyEnter } from '../../utils/keyEvent';
|
||||||
|
|
||||||
import RenameClientModal from './rename-client-modal/RenameClientModal';
|
import RenameClientModal from './rename-client-modal/RenameClientModal';
|
||||||
|
|
||||||
@@ -53,7 +54,6 @@ function NavigationMenu() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isKeyEnter = (event: KeyboardEvent<HTMLDivElement>) => event.key === 'Enter';
|
|
||||||
const handleFullscreen = () => toggleFullScreen();
|
const handleFullscreen = () => toggleFullScreen();
|
||||||
const handleMirror = () => toggleMirror();
|
const handleMirror = () => toggleMirror();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { ProjectFileListResponse } from 'ontime-types';
|
||||||
|
|
||||||
|
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||||
|
import { PROJECT_LIST } from '../api/apiConstants';
|
||||||
|
import { getProjects } from '../api/ontimeApi';
|
||||||
|
|
||||||
|
const placeholderProjectList: ProjectFileListResponse = {
|
||||||
|
files: [],
|
||||||
|
lastLoadedProject: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useProjectList() {
|
||||||
|
const { data, status } = useQuery({
|
||||||
|
queryKey: PROJECT_LIST,
|
||||||
|
queryFn: getProjects,
|
||||||
|
placeholderData: placeholderProjectList,
|
||||||
|
retry: 5,
|
||||||
|
retryDelay: (attempt: number) => attempt * 2500,
|
||||||
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
|
networkMode: 'always',
|
||||||
|
});
|
||||||
|
return { data: data ?? placeholderProjectList, status };
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { KeyboardEvent } from 'react';
|
||||||
|
|
||||||
|
export function isKeyEnter<T>(event: KeyboardEvent<T>): boolean {
|
||||||
|
return event.key === 'Enter';
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { ErrorBoundary } from '@sentry/react';
|
|||||||
import { useKeyDown } from '../../common/hooks/useKeyDown';
|
import { useKeyDown } from '../../common/hooks/useKeyDown';
|
||||||
|
|
||||||
import AboutPanel from './panel/about-panel/AboutPanel';
|
import AboutPanel from './panel/about-panel/AboutPanel';
|
||||||
|
import ProjectPanel from './panel/project-panel/ProjectPanel';
|
||||||
import PanelContent from './panel-content/PanelContent';
|
import PanelContent from './panel-content/PanelContent';
|
||||||
import PanelList from './panel-list/PanelList';
|
import PanelList from './panel-list/PanelList';
|
||||||
import { useSettingsStore } from './settingsStore';
|
import { useSettingsStore } from './settingsStore';
|
||||||
@@ -22,7 +23,10 @@ export default function AppSettings() {
|
|||||||
<div className={style.container}>
|
<div className={style.container}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<PanelList />
|
<PanelList />
|
||||||
<PanelContent onClose={closeSettings}>{selectedPanel === 'about' && <AboutPanel />}</PanelContent>
|
<PanelContent onClose={closeSettings}>
|
||||||
|
{selectedPanel === 'project' && <ProjectPanel />}
|
||||||
|
{selectedPanel === 'about' && <AboutPanel />}
|
||||||
|
</PanelContent>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { KeyboardEvent } from 'react';
|
import { Fragment } from 'react';
|
||||||
|
|
||||||
|
import { isKeyEnter } from '../../../common/utils/keyEvent';
|
||||||
import { cx } from '../../../common/utils/styleUtils';
|
import { cx } from '../../../common/utils/styleUtils';
|
||||||
import { settingPanels, SettingsOption, useSettingsStore } from '../settingsStore';
|
import { settingPanels, SettingsOption, useSettingsStore } from '../settingsStore';
|
||||||
|
|
||||||
@@ -12,8 +13,6 @@ export default function PanelList() {
|
|||||||
setShowSettings(panel.id);
|
setShowSettings(panel.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isKeyEnter = (event: KeyboardEvent<HTMLLIElement>) => event.key === 'Enter';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className={style.tabs}>
|
<ul className={style.tabs}>
|
||||||
{settingPanels.map((panel) => {
|
{settingPanels.map((panel) => {
|
||||||
@@ -27,7 +26,7 @@ export default function PanelList() {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Fragment key={panel.id}>
|
||||||
<li
|
<li
|
||||||
key={panel.id}
|
key={panel.id}
|
||||||
onClick={() => handleSelect(panel)}
|
onClick={() => handleSelect(panel)}
|
||||||
@@ -47,7 +46,7 @@ export default function PanelList() {
|
|||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -10,7 +10,11 @@
|
|||||||
|
|
||||||
.subheader {
|
.subheader {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
|
padding-bottom: 0.25rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
@@ -35,3 +39,29 @@
|
|||||||
display: block;
|
display: block;
|
||||||
color: $error-red;
|
color: $error-red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: calc(1rem - 2px);
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
tr {
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
border-bottom: 1px solid $white-10;
|
||||||
|
font-weight: 400;
|
||||||
|
color: $gray-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:nth-child(even) {
|
||||||
|
background-color: $white-1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export function SubHeader({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Section({ children }: { children: ReactNode }) {
|
export function Section({ children }: { children: ReactNode }) {
|
||||||
return <p className={style.section}>{children}</p>;
|
return <div className={style.section}>{children}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Paragraph({ children }: { children: ReactNode }) {
|
export function Paragraph({ children }: { children: ReactNode }) {
|
||||||
@@ -21,3 +21,7 @@ export function Paragraph({ children }: { children: ReactNode }) {
|
|||||||
export function Card({ children }: { children: ReactNode }) {
|
export function Card({ children }: { children: ReactNode }) {
|
||||||
return <div className={style.card}>{children}</div>;
|
return <div className={style.card}>{children}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Table({ children }: { children: ReactNode }) {
|
||||||
|
return <table className={style.table}>{children}</table>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { IconButton, Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/react';
|
||||||
|
import { IoEllipsisHorizontal } from '@react-icons/all-files/io5/IoEllipsisHorizontal';
|
||||||
|
|
||||||
|
import { useProjectList } from '../../../../common/hooks-query/useProjectList';
|
||||||
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
|
import style from './ProjectPanel.module.scss';
|
||||||
|
|
||||||
|
export default function ProjectList() {
|
||||||
|
const { data } = useProjectList();
|
||||||
|
const { files, lastLoadedProject } = data;
|
||||||
|
|
||||||
|
// extract currently loaded from file list
|
||||||
|
const currentlyLoadedIndex = files.findIndex((project) => project.filename === lastLoadedProject);
|
||||||
|
const projectFiles = [...files];
|
||||||
|
const current = projectFiles.splice(currentlyLoadedIndex, 1)[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel.Table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Project Name</th>
|
||||||
|
<th>Date Created</th>
|
||||||
|
<th>Date Modified</th>
|
||||||
|
<th />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{current && (
|
||||||
|
<tr className={style.current}>
|
||||||
|
<td>{current.filename}</td>
|
||||||
|
<td>{new Date(current.createdAt).toLocaleString()}</td>
|
||||||
|
<td>{new Date(current.updatedAt).toLocaleString()}</td>
|
||||||
|
<td className={style.actionButton}>
|
||||||
|
<ActionMenu />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{projectFiles.map((project) => {
|
||||||
|
const createdAt = new Date(project.createdAt).toLocaleString();
|
||||||
|
const updatedAt = new Date(project.updatedAt).toLocaleString();
|
||||||
|
return (
|
||||||
|
<tr key={project.filename}>
|
||||||
|
<td>{project.filename}</td>
|
||||||
|
<td>{createdAt}</td>
|
||||||
|
<td>{updatedAt}</td>
|
||||||
|
<td className={style.actionButton}>
|
||||||
|
<ActionMenu />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</Panel.Table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ActionMenu() {
|
||||||
|
return (
|
||||||
|
<Menu variant='ontime-on-dark' size='sm'>
|
||||||
|
<MenuButton
|
||||||
|
as={IconButton}
|
||||||
|
aria-label='Options'
|
||||||
|
icon={<IoEllipsisHorizontal />}
|
||||||
|
variant='ontime-ghosted'
|
||||||
|
size='sm'
|
||||||
|
/>
|
||||||
|
<MenuList>
|
||||||
|
<MenuItem>Load</MenuItem>
|
||||||
|
<MenuItem>Rename</MenuItem>
|
||||||
|
<MenuItem>Duplicate</MenuItem>
|
||||||
|
<MenuItem>Delete</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
@use '../../../../theme/ontimeColours' as *;
|
||||||
|
|
||||||
|
.current {
|
||||||
|
color: $blue-400;
|
||||||
|
background-color: $gray-1350;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionButton {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Button } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
|
import ProjectList from './ProjectList';
|
||||||
|
|
||||||
|
export default function ProjectPanel() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Panel.Header>Project</Panel.Header>
|
||||||
|
<Panel.Section>
|
||||||
|
<Panel.Card>
|
||||||
|
<Panel.SubHeader>
|
||||||
|
Manage projects
|
||||||
|
<Button variant='ontime-filled'>New</Button>
|
||||||
|
</Panel.SubHeader>
|
||||||
|
<ProjectList />
|
||||||
|
</Panel.Card>
|
||||||
|
</Panel.Section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,22 +8,26 @@ export type SettingsOption = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const settingPanels: SettingsOption[] = [
|
export const settingPanels: SettingsOption[] = [
|
||||||
{ id: 'project', label: 'Project' },
|
{
|
||||||
|
id: 'project',
|
||||||
|
label: 'Project',
|
||||||
|
secondary: [{ id: 'project__manage', label: 'Manage project files' }],
|
||||||
|
},
|
||||||
{ id: 'general', label: 'General' },
|
{ id: 'general', label: 'General' },
|
||||||
{ id: 'interface', label: 'Interface' },
|
{ id: 'interface', label: 'Interface' },
|
||||||
{ id: 'views', label: 'Views' },
|
{ id: 'views', label: 'Views' },
|
||||||
{
|
{
|
||||||
id: 'sources',
|
id: 'sources',
|
||||||
label: 'Data Sources',
|
label: 'Data Sources',
|
||||||
secondary: [{ id: 'g-sheet', label: 'Sync with Google Sheet' }],
|
secondary: [{ id: 'sources__gsheet', label: 'Sync with Google Sheet' }],
|
||||||
split: true,
|
split: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'integrations',
|
id: 'integrations',
|
||||||
label: 'Integrations',
|
label: 'Integrations',
|
||||||
secondary: [
|
secondary: [
|
||||||
{ id: 'osc', label: 'OSC Integration' },
|
{ id: 'integrations__osc', label: 'OSC Integration' },
|
||||||
{ id: 'http', label: 'HTTP Integration' },
|
{ id: 'integrations__http', label: 'HTTP Integration' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ id: 'log', label: 'Log', split: true },
|
{ id: 'log', label: 'Log', split: true },
|
||||||
@@ -31,10 +35,6 @@ export const settingPanels: SettingsOption[] = [
|
|||||||
id: 'about',
|
id: 'about',
|
||||||
label: 'About',
|
label: 'About',
|
||||||
split: true,
|
split: true,
|
||||||
secondary: [
|
|
||||||
{ id: 'links', label: 'Links' },
|
|
||||||
{ id: 'version', label: 'Version' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@use "../../../../theme/_ontimeColours" as *;
|
@use '../../../../theme/_ontimeColours' as *;
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -9,6 +9,14 @@
|
|||||||
.rundownPreview {
|
.rundownPreview {
|
||||||
font-size: calc(1rem - 2px);
|
font-size: calc(1rem - 2px);
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
|
tr td:first-child,
|
||||||
|
tr th:first-child {
|
||||||
|
position: sticky;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2;
|
||||||
|
background-color: white;
|
||||||
|
box-shadow: 1px 0 $gray-50;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.header,
|
.header,
|
||||||
@@ -60,12 +68,3 @@
|
|||||||
background-color: $gray-50;
|
background-color: $gray-50;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table tr td:first-child,
|
|
||||||
table tr th:first-child {
|
|
||||||
position: sticky;
|
|
||||||
left: 0;
|
|
||||||
z-index: 2;
|
|
||||||
background-color: white;
|
|
||||||
box-shadow: 1px 0 $gray-50;
|
|
||||||
}
|
|
||||||
@@ -19,7 +19,7 @@ export type ProjectFile = {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProjectFileList = Array<ProjectFile>;
|
export type ProjectFileList = ProjectFile[];
|
||||||
|
|
||||||
export type ProjectFileListResponse = {
|
export type ProjectFileListResponse = {
|
||||||
files: ProjectFileList;
|
files: ProjectFileList;
|
||||||
|
|||||||
Reference in New Issue
Block a user