mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-08 15:59:16 +00:00
Merge import (#1164)
--------- Co-authored-by: Carlos Valente <carlosvalente@pm.me>
This commit is contained in:
committed by
GitHub
parent
0abaf7724d
commit
aa4ee546ec
@@ -11,7 +11,7 @@ const dbPath = `${apiEntryUrl}/db`;
|
|||||||
/**
|
/**
|
||||||
* HTTP request to the current DB
|
* HTTP request to the current DB
|
||||||
*/
|
*/
|
||||||
async function getDb(filename: string): Promise<AxiosResponse<DatabaseModel>> {
|
export function getDb(filename: string): Promise<AxiosResponse<DatabaseModel>> {
|
||||||
return axios.post(`${dbPath}/download/`, { filename });
|
return axios.post(`${dbPath}/download/`, { filename });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export default function ManageProjects() {
|
|||||||
const errorMessage = maybeAxiosError(error);
|
const errorMessage = maybeAxiosError(error);
|
||||||
setError(`Error uploading file: ${errorMessage}`);
|
setError(`Error uploading file: ${errorMessage}`);
|
||||||
} finally {
|
} finally {
|
||||||
invalidateAllCaches();
|
await invalidateAllCaches();
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(null);
|
setLoading(null);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export type ProjectFormValues = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface ProjectFormProps {
|
interface ProjectFormProps {
|
||||||
action: 'duplicate' | 'rename';
|
action: 'duplicate' | 'rename' | 'merge';
|
||||||
filename: string;
|
filename: string;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onSubmit: (values: ProjectFormValues) => Promise<void>;
|
onSubmit: (values: ProjectFormValues) => Promise<void>;
|
||||||
|
|||||||
@@ -11,13 +11,15 @@ import {
|
|||||||
renameProject,
|
renameProject,
|
||||||
} from '../../../../common/api/db';
|
} from '../../../../common/api/db';
|
||||||
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/utils';
|
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/utils';
|
||||||
|
import { cx } from '../../../../common/utils/styleUtils';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
import ProjectForm, { ProjectFormValues } from './ProjectForm';
|
import ProjectForm, { ProjectFormValues } from './ProjectForm';
|
||||||
|
import ProjectMergeForm from './ProjectMergeForm';
|
||||||
|
|
||||||
import style from './ProjectPanel.module.scss';
|
import style from './ProjectPanel.module.scss';
|
||||||
|
|
||||||
export type EditMode = 'rename' | 'duplicate' | null;
|
export type EditMode = 'rename' | 'duplicate' | 'merge' | null;
|
||||||
|
|
||||||
interface ProjectListItemProps {
|
interface ProjectListItemProps {
|
||||||
current?: boolean;
|
current?: boolean;
|
||||||
@@ -100,8 +102,10 @@ export default function ProjectListItem({
|
|||||||
handleToggleEditMode(null, null);
|
handleToggleEditMode(null, null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isCurrentlyBeingEdited = editingMode && filename === editingFilename;
|
const isCurrentlyBeingEdited = filename === editingFilename;
|
||||||
const classes = current && !isCurrentlyBeingEdited ? style.current : undefined;
|
const showProjectForm = (editingMode === 'rename' || editingMode === 'duplicate') && filename === editingFilename;
|
||||||
|
const showMergeForm = editingMode === 'merge' && isCurrentlyBeingEdited;
|
||||||
|
const classes = cx([current && !isCurrentlyBeingEdited && style.current, isCurrentlyBeingEdited && style.isEditing]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -113,7 +117,7 @@ export default function ProjectListItem({
|
|||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
<tr key={filename} className={classes}>
|
<tr key={filename} className={classes}>
|
||||||
{isCurrentlyBeingEdited ? (
|
{showProjectForm ? (
|
||||||
<td colSpan={99}>
|
<td colSpan={99}>
|
||||||
<ProjectForm
|
<ProjectForm
|
||||||
action={editingMode}
|
action={editingMode}
|
||||||
@@ -125,7 +129,7 @@ export default function ProjectListItem({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<td className={style.containCell}>{filename}</td>
|
<td className={style.containCell}>{filename}</td>
|
||||||
<td>{new Date(updatedAt).toLocaleString()}</td>
|
<td>{current ? 'Currently loaded' : new Date(updatedAt).toLocaleString()}</td>
|
||||||
<td className={style.actionButton}>
|
<td className={style.actionButton}>
|
||||||
<ActionMenu
|
<ActionMenu
|
||||||
current={current}
|
current={current}
|
||||||
@@ -133,12 +137,20 @@ export default function ProjectListItem({
|
|||||||
onChangeEditMode={handleToggleEditMode}
|
onChangeEditMode={handleToggleEditMode}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onLoad={handleLoad}
|
onLoad={handleLoad}
|
||||||
isDisabled={loading}
|
isDisabled={loading || showMergeForm}
|
||||||
|
onMerge={(filename) => handleToggleEditMode('merge', filename)}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
|
{showMergeForm && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={99}>
|
||||||
|
<ProjectMergeForm onClose={handleCancel} fileName={filename} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -148,11 +160,12 @@ interface ActionMenuProps {
|
|||||||
filename: string;
|
filename: string;
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
onChangeEditMode: (editMode: EditMode, filename: string) => void;
|
onChangeEditMode: (editMode: EditMode, filename: string) => void;
|
||||||
onDelete: (filename: string) => void;
|
onDelete: (filename: string) => Promise<void>;
|
||||||
onLoad: (filename: string) => void;
|
onLoad: (filename: string) => Promise<void>;
|
||||||
|
onMerge: (filename: string) => void;
|
||||||
}
|
}
|
||||||
function ActionMenu(props: ActionMenuProps) {
|
function ActionMenu(props: ActionMenuProps) {
|
||||||
const { current, filename, isDisabled, onChangeEditMode, onDelete, onLoad } = props;
|
const { current, filename, isDisabled, onChangeEditMode, onDelete, onLoad, onMerge } = props;
|
||||||
|
|
||||||
const handleRename = () => {
|
const handleRename = () => {
|
||||||
onChangeEditMode('rename', filename);
|
onChangeEditMode('rename', filename);
|
||||||
@@ -185,6 +198,9 @@ function ActionMenu(props: ActionMenuProps) {
|
|||||||
<MenuItem onClick={() => onLoad(filename)} isDisabled={current}>
|
<MenuItem onClick={() => onLoad(filename)} isDisabled={current}>
|
||||||
Load
|
Load
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => onMerge(filename)} isDisabled={current}>
|
||||||
|
Partial Load
|
||||||
|
</MenuItem>
|
||||||
<MenuItem onClick={handleRename}>Rename</MenuItem>
|
<MenuItem onClick={handleRename}>Rename</MenuItem>
|
||||||
<MenuItem onClick={handleDuplicate}>Duplicate</MenuItem>
|
<MenuItem onClick={handleDuplicate}>Duplicate</MenuItem>
|
||||||
<MenuItem onClick={handleDownload}>Download</MenuItem>
|
<MenuItem onClick={handleDownload}>Download</MenuItem>
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { Button, Switch } from '@chakra-ui/react';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { PROJECT_DATA } from '../../../../common/api/constants';
|
||||||
|
import { getDb, patchData } from '../../../../common/api/db';
|
||||||
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
|
import { makeProjectPatch } from './project.utils';
|
||||||
|
|
||||||
|
import style from './ProjectPanel.module.scss';
|
||||||
|
|
||||||
|
interface ProjectMergeFromProps {
|
||||||
|
onClose: () => void;
|
||||||
|
fileName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectMergeFormValues = {
|
||||||
|
project: boolean;
|
||||||
|
rundown: boolean;
|
||||||
|
viewSettings: boolean;
|
||||||
|
urlPresets: boolean;
|
||||||
|
osc: boolean;
|
||||||
|
http: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ProjectMergeForm(props: ProjectMergeFromProps) {
|
||||||
|
const { onClose, fileName } = props;
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
register,
|
||||||
|
formState: { isSubmitting, isValid, isDirty },
|
||||||
|
} = useForm<ProjectMergeFormValues>({
|
||||||
|
defaultValues: {
|
||||||
|
project: false,
|
||||||
|
rundown: false,
|
||||||
|
viewSettings: false,
|
||||||
|
urlPresets: false,
|
||||||
|
osc: false,
|
||||||
|
http: false,
|
||||||
|
},
|
||||||
|
resetOptions: {
|
||||||
|
keepDirtyValues: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmitCreate = async (values: ProjectMergeFormValues) => {
|
||||||
|
const allFalse = Object.values(values).every((value) => !value);
|
||||||
|
if (allFalse) {
|
||||||
|
setError('At least one option must be selected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// make patch object
|
||||||
|
const { data } = await getDb(fileName);
|
||||||
|
const patch = await makeProjectPatch(data, values);
|
||||||
|
|
||||||
|
// request patch
|
||||||
|
await patchData(patch);
|
||||||
|
await queryClient.invalidateQueries({ queryKey: PROJECT_DATA });
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
setError(maybeAxiosError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel.Section as='form' onSubmit={handleSubmit(handleSubmitCreate)}>
|
||||||
|
<Panel.Title>
|
||||||
|
Merge {`"${fileName}"`}
|
||||||
|
<div className={style.createActionButtons}>
|
||||||
|
<Button onClick={onClose} variant='ontime-ghosted' size='sm' isDisabled={isSubmitting}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isDisabled={!isValid || !isDirty}
|
||||||
|
type='submit'
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
>
|
||||||
|
Merge
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Panel.Title>
|
||||||
|
{error && <Panel.Error>{error}</Panel.Error>}
|
||||||
|
<div className={style.innerColumn}>
|
||||||
|
<Panel.Description>
|
||||||
|
Select partial data from {`"${fileName}"`} to merge into the current project.
|
||||||
|
<br /> This process is irreversible.
|
||||||
|
</Panel.Description>
|
||||||
|
<label>
|
||||||
|
<Switch variant='ontime' {...register('project')} />
|
||||||
|
Project data
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<Switch variant='ontime' {...register('rundown')} />
|
||||||
|
Rundown + Custom Fields
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<Switch variant='ontime' {...register('viewSettings')} />
|
||||||
|
View Settings
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<Switch variant='ontime' {...register('urlPresets')} />
|
||||||
|
URL Presets
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<Switch variant='ontime' {...register('osc')} />
|
||||||
|
OSC Integration
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<Switch variant='ontime' {...register('http')} />
|
||||||
|
HTTP Integration
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</Panel.Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,10 @@
|
|||||||
background-color: $blue-1100;
|
background-color: $blue-1100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.isEditing {
|
||||||
|
color: $blue-500;
|
||||||
|
}
|
||||||
|
|
||||||
.actionButton {
|
.actionButton {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
@@ -49,4 +53,9 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { DatabaseModel, isKeyOfType } from 'ontime-types';
|
||||||
|
|
||||||
|
export async function makeProjectPatch(data: DatabaseModel, mergeKeys: Record<string, boolean>) {
|
||||||
|
const patchObject: Partial<DatabaseModel> = {};
|
||||||
|
|
||||||
|
for (const key in mergeKeys) {
|
||||||
|
if (isKeyOfType(key, data) && mergeKeys[key]) {
|
||||||
|
// if the rundown is merged we also need the custom fields
|
||||||
|
if (key === 'rundown') {
|
||||||
|
patchObject.customFields = data['customFields'];
|
||||||
|
}
|
||||||
|
Object.assign(patchObject, { [key]: data[key] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return patchObject;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user