Merge import (#1164)

---------

Co-authored-by: Carlos Valente <carlosvalente@pm.me>
This commit is contained in:
Alex Christoffer Rasmussen
2024-09-01 20:38:59 +02:00
committed by GitHub
parent 0abaf7724d
commit aa4ee546ec
7 changed files with 181 additions and 12 deletions
+1 -1
View File
@@ -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;
}