frontend/app/CommissionsList/CommissionEntryEditComponent.tsx (706 lines of code) (raw):
import React, { useState, useEffect } from "react";
import { RouteComponentProps, useHistory } from "react-router";
import { Breadcrumb } from "@guardian/pluto-headers";
import {
Button,
CircularProgress,
FormControl,
FormLabel,
Grid,
Paper,
TextField,
Typography,
Tooltip,
Dialog,
DialogContent,
DialogContentText,
DialogActions,
Input,
InputLabel,
Box,
FormControlLabel,
Checkbox,
} from "@material-ui/core";
import {
SystemNotification,
SystemNotifcationKind,
} from "@guardian/pluto-headers";
import {
KeyboardDatePicker,
MuiPickersUtilsProvider,
} from "@material-ui/pickers";
import DateFnsUtils from "@date-io/date-fns";
import ParseDateISO from "date-fns/parseISO";
import FormatDateISO from "date-fns/formatISO";
import FormatDate from "date-fns/format";
import {
loadCommissionData,
projectsForCommission,
updateCommissionData,
} from "./helpers";
import ErrorIcon from "@material-ui/icons/Error";
import ProductionOfficeSelector from "../common/ProductionOfficeSelector";
import WorkingGroupSelector from "../common/WorkingGroupSelector";
import StatusSelector from "../common/StatusSelector";
import ProjectsTable from "../ProjectEntryList/ProjectsTable";
import { Helmet } from "react-helmet";
import HelpIcon from "@material-ui/icons/Help";
import CommissionEntryDeliverablesComponent from "./CommissionEntryDeliverablesComponent";
import UsersAutoComplete from "../common/UsersAutoComplete";
import { useGuardianStyles } from "~/misc/utils";
import ProjectFilterComponent from "~/filter/ProjectFilterComponent";
import { filterTermsToQuerystring } from "~/filter/terms";
import { isLoggedIn } from "~/utils/api";
import { set } from "js-cookie";
import { SortDirection } from "~/utils/lists";
declare var deploymentRootPath: string;
interface CommissionEntryFormProps {
commission: CommissionFullRecord;
workingGroupName: string;
isSaving: boolean;
onSubmit: (updatedCommission: CommissionFullRecord) => void;
}
const CommissionEntryForm: React.FC<CommissionEntryFormProps> = ({
commission,
isSaving,
onSubmit,
}) => {
const [formState, setFormState] = useState(commission);
const classes = useGuardianStyles();
useEffect(() => {
setFormState(commission); // Initialize form state with commission data
}, [commission]);
const handleChange = (field: keyof CommissionFullRecord, value: any) => {
const updatedFormState = { ...formState, [field]: value };
setFormState((prevState) => ({ ...prevState, [field]: value }));
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
onSubmit(formState);
};
const hasUnsavedChanges =
JSON.stringify(formState) !== JSON.stringify(commission);
const handlePrivateChange = (value: any) => {
if (value) {
setFormState((prevState) => ({ ...prevState, ["confidential"]: false }));
} else {
setFormState((prevState) => ({ ...prevState, ["confidential"]: true }));
}
};
const isCommissionOlderThanOneDay = () => {
const createdUNIX = new Date(commission.created).getTime();
const currentUNIX = Date.now();
return currentUNIX - createdUNIX >= 86400000;
};
const newStatusIsCompleted = () => formState.status === "Completed";
const abortChanges = () => {
if (commission) {
setFormState(commission);
}
};
return (
<form onSubmit={handleSubmit} className={classes.root}>
<Grid container xs={12} direction="row" spacing={3}>
{/*left-hand column*/}
<Grid item xs={6}>
<TextField
id="title"
label="Title"
value={formState.title}
onChange={(evt) => handleChange("title", evt.target.value)}
/>
<WorkingGroupSelector
workingGroupId={formState.workingGroupId}
onChange={(evt) => handleChange("workingGroupId", evt.target.value)}
/>
<TextField
id="original-commissioner"
label="Originally commissioned by"
value={formState.originalCommissionerName}
disabled={true}
/>
<TextField
id="description"
label="Description/Brief"
multiline={true}
value={formState.description ?? ""}
onChange={(evt) => handleChange("description", evt.target.value)}
/>
<FormControl>
<FormLabel htmlFor="scheduled-completion">
Scheduled completion
</FormLabel>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<KeyboardDatePicker
variant="inline"
format="yyyy-MM-dd"
margin="normal"
id="scheduled-completion"
value={formState.scheduledCompletion}
onChange={(value) => {
if (value) {
const formattedDate = FormatDateISO(value); // Format the date to ISO string
handleChange("scheduledCompletion", formattedDate);
}
}}
/>
</MuiPickersUtilsProvider>
</FormControl>
</Grid>
{/*right-hand column*/}
<Grid item xs={6}>
<UsersAutoComplete
valueDidChange={(evt, newValue) =>
handleChange("owner", newValue?.join("|") ?? "")
}
label="Owner"
value={formState.owner}
shouldValidate={true}
/>
<TextField
id="created"
label="Created"
value={commission.created ?? "(error)"}
disabled={true}
/>
<StatusSelector
value={formState.status}
onChange={(evt: any) => handleChange("status", evt.target.value)}
/>
<ProductionOfficeSelector
label="Production Office"
value={formState.productionOffice}
onChange={(evt: any) =>
handleChange("productionOffice", evt.target.value)
}
/>
<TextField
id="notes"
label="Notes"
value={formState.notes ?? ""}
multiline={true}
onChange={(evt) => handleChange("notes", evt.target.value)}
/>
{commission.googleFolder ? (
<div>
View Documents
<br />
{commission.googleFolder ? (
<Tooltip title="Open commission folder in Google Drive" arrow>
<a href={commission.googleFolder} target="_blank">
<img
className="smallicon"
src="/pluto-core/assets/images/google-drive-folder-icon.png"
/>
</a>
</Tooltip>
) : (
<div>
<div className={classes.noGoogleText}>None</div>
<Tooltip
title="We have not implemented the functionality to create Google Drive folders due to some limitations and technical issues. If you would like the functionality please request it from multimediatech@theguardian.com"
arrow
>
<HelpIcon className={classes.warningIcon} />
</Tooltip>
</div>
)}
</div>
) : null}
<br />
<br />
<Tooltip title="Select this option if this is a sensitive commission and you do not want other users besides the commission owners to be able to view or access this commission via Pluto.">
<FormControlLabel
control={
<Checkbox
checked={formState.confidential}
name="confidential"
color="primary"
onChange={(evt) =>
handlePrivateChange(formState.confidential)
}
/>
}
label="Private"
/>
</Tooltip>
{hasUnsavedChanges && !isSaving ? (
isCommissionOlderThanOneDay() ? (
<div className={classes.formButtons}>
<Button type="submit" variant="contained" color="secondary">
Save changes
</Button>
</div>
) : newStatusIsCompleted() ? (
<div>
<Typography style={{ marginTop: "30px" }}>
Are you sure you want to set this commission to Completed so
soon? If you have just copied a large number of files to a
project asset folder then please wait another day before
setting this to Completed, to ensure all the files for this
commission have been properly backed up.
</Typography>
<div className={classes.formButtons}>
<Button
color="secondary"
variant="contained"
onClick={() => abortChanges()}
>
Come back later
</Button>
<Button type="submit" variant="contained" color="secondary">
Save changes
</Button>
</div>
</div>
) : (
<div className={classes.formButtons}>
<Button type="submit" variant="contained" color="secondary">
Save changes
</Button>
</div>
)
) : null}
{isSaving ? (
<div className={classes.formButtons}>
<CircularProgress style={{ width: "18px", height: "18px" }} />
</div>
) : null}
</Grid>
</Grid>
</form>
);
};
interface RouteComponentMatches {
commissionId: string;
}
const CommissionEntryEditComponent: React.FC<RouteComponentProps<
RouteComponentMatches
>> = (props) => {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [commissionData, setCommissionData] = useState<
CommissionFullRecord | undefined
>(undefined);
const [projectList, setProjectList] = useState<Project[] | undefined>(
undefined
);
const history = useHistory();
const [lastError, setLastError] = useState<null | string>(null);
const [isSaving, setIsSaving] = useState<boolean>(false);
const classes = useGuardianStyles();
const [errorDialog, setErrorDialog] = useState<boolean>(false);
const [filterTerms, setFilterTerms] = useState<ProjectFilterTerms>({
commissionId: parseInt(props.match.params.commissionId),
match: "W_STARTSWITH",
});
const [user, setUser] = useState<PlutoUser | null>(null);
const [projectCount, setProjectCount] = useState<number>(0);
const [deliverablesSearchString, setDeliverablesSearchString] = useState<
string
>("");
const [isAdmin, setIsAdmin] = useState<boolean>(false);
const [userAllowedBoolean, setUserAllowedBoolean] = useState<boolean>(true);
const [order, setOrder] = useState<SortDirection>("desc");
const [orderBy, setOrderBy] = useState<keyof Project>("created");
useEffect(() => {
const fetchCommissionData = async () => {
try {
const commissionId = parseInt(props.match.params.commissionId); // Parse the commissionId from string to number
const data = await loadCommissionData(commissionId);
setCommissionData(data);
} catch (error) {
SystemNotification.open(
SystemNotifcationKind.Error,
"Failed to load commission data."
);
setErrorDialog(true);
} finally {
setIsLoading(false);
}
};
fetchCommissionData();
}, [props.match.params.commissionId, history]);
let commissionId: number;
try {
commissionId = parseInt(props.match.params.commissionId);
} catch (err) {
console.error(
"Could not convert ",
props.match.params.commissionId,
" into a number"
);
commissionId = -1;
}
const handleFormSubmit = async (updatedCommission: CommissionFullRecord) => {
setIsSaving(true);
try {
await updateCommissionData(updatedCommission);
SystemNotification.open(
SystemNotifcationKind.Success,
"Commission saved successfully."
);
setCommissionData(updatedCommission); // Update commission data with saved changes
} catch (error) {
SystemNotification.open(
SystemNotifcationKind.Error,
"Failed to save commission."
);
} finally {
setIsSaving(false);
}
};
/**
* load in member projects on launch
*/
useEffect(() => {
const doLoadIn = () => {
projectsForCommission(commissionId, 0, 5, filterTerms, order, orderBy)
.then(([projects, count]) => {
setProjectList(projects);
setProjectCount(count);
setLastError(null);
})
.catch((err) => {
if (err.hasOwnProperty("response")) {
console.error(
"Server returned an error loading projects list: ",
err.response
);
switch (err.response.status) {
case 400:
setLastError(
"Could not load projects, client-side search error"
);
break;
case 500:
console.log("Server said", err.response.body);
setLastError(
"Could not load projects, server error, see console"
);
break;
case 503:
case 504:
setLastError("Server not responding, retrying...");
window.setTimeout(doLoadIn, 1000);
break;
}
} else {
console.error("Browser error trying to load projects list: ", err);
setLastError("Browser error loading projects list");
}
});
};
doLoadIn();
}, [filterTerms]);
/**
* load in commission data on launch
*/
useEffect(() => {
const doLoadIn = () => {
loadCommissionData(commissionId)
.then((comm) => {
setCommissionData(comm);
setIsLoading(false);
})
.catch((err) => {
console.error("Could not load commission: ", err);
setIsLoading(false);
if (err.hasOwnProperty("response")) {
console.log("Error was from a response, ", err.response);
switch (err.response.status) {
case 404:
setLastError("This commission does not exist");
setErrorDialog(true);
break;
case 500:
setLastError(`Server error: ${err.response.body}`);
break;
case 503:
case 504:
setLastError("Server is not responding");
window.setTimeout(doLoadIn, 1000); //try again in 1s
break;
default:
setLastError(`Server returned ${err.response.status}`);
break;
}
} else {
console.error("Could not load in commission data: ", err);
setLastError(err.toString());
}
});
};
doLoadIn();
const fetchWhoIsLoggedIn = async () => {
try {
const loggedIn = await isLoggedIn();
setIsAdmin(loggedIn.isAdmin);
} catch {
setIsAdmin(false);
}
};
fetchWhoIsLoggedIn();
}, []);
/**
* if the commission data changes, then load in the contents
*/
useEffect(() => {}, [commissionData]);
const closeDialog = () => {
setErrorDialog(false);
props.history.goBack();
};
useEffect(() => {
const fetchWhoIsLoggedIn = async () => {
try {
let user = await isLoggedIn();
user.uid = generateUserName(user.uid);
setUser(user);
} catch (error) {
console.error("Could not login user:", error);
}
};
fetchWhoIsLoggedIn();
}, []);
const stringUpdated = (
event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
) => {
const newValue = event.target.value;
setDeliverablesSearchString(newValue);
};
const generateUserName = (inputString: string) => {
if (inputString.includes("@")) {
const splitString = inputString.split("@", 1)[0];
const userNameConst = splitString.replace(".", "_");
return userNameConst;
}
return inputString;
};
useEffect(() => {
const userAllowed = async () => {
try {
const loggedIn = await isLoggedIn();
if (loggedIn.isAdmin) {
setUserAllowedBoolean(true);
} else if (
commissionData?.owner
.split("|")
.includes(generateUserName(loggedIn.uid).toLowerCase())
) {
setUserAllowedBoolean(true);
} else {
setUserAllowedBoolean(false);
}
} catch {
console.error(
"Error attempting to check if user is allowed access to this page."
);
}
};
if (commissionData?.confidential) {
userAllowed();
}
}, [commissionData?.owner]);
return (
<>
{userAllowedBoolean ? (
<>
{commissionData ? (
<Helmet>
<title>[{commissionData.title}] Details</title>
</Helmet>
) : null}
<Grid
container
justifyContent="space-between"
style={{ marginBottom: "0.2em" }}
spacing={3}
>
<Grid item>
<Breadcrumb
commissionId={commissionId}
plutoCoreBaseUri={
deploymentRootPath.endsWith("/")
? deploymentRootPath.substr(
0,
deploymentRootPath.length - 1
)
: deploymentRootPath
}
/>
</Grid>
<Grid item xs={5}>
<Box display="flex" justifyContent="flex-end">
{isAdmin ? (
<div>
<Button
href={
"/pluto-core/commission/" + commissionId + "/deletedata"
}
color="secondary"
variant="contained"
>
Delete Data
</Button>
</div>
) : null}
</Box>
</Grid>
</Grid>
<Paper elevation={3}>
{isLoading ? (
<Grid
container
direction="row"
justifyContent="space-around"
alignContent="center"
>
<Grid item xs>
<CircularProgress
className={classes.inlineThrobber}
color="secondary"
/>
<Typography className={classes.inlineText}>
Loading...
</Typography>
</Grid>
</Grid>
) : null}
{lastError ? (
<Grid container direction="row" justifyContent="space-around">
<Grid item xs className={classes.errorBlock}>
<ErrorIcon className={classes.inlineThrobber} />
<Typography className={classes.inlineText}>
{lastError}
</Typography>
</Grid>
</Grid>
) : null}
{commissionData ? (
<CommissionEntryForm
commission={commissionData}
workingGroupName="test"
isSaving={isSaving}
onSubmit={handleFormSubmit} // Handle the updated commission data
/>
) : null}
</Paper>
{/*will repace this with an icon*/}
<Grid container direction="row" justifyContent="space-between">
<Grid item>
<Typography variant="h4">Projects</Typography>
</Grid>
<Grid item>
<Button
variant="contained"
color="primary"
onClick={() =>
history.push(
`/project/new?commissionId=${commissionId}&workingGroupId=${commissionData?.workingGroupId}`
)
}
>
New Project
</Button>
</Grid>
</Grid>
<Grid container>
{filterTerms ? (
<Grid item>
<ProjectFilterComponent
filterTerms={filterTerms}
filterDidUpdate={(newFilters: ProjectFilterTerms) => {
console.log(
"ProjectFilterComponent filterDidUpdate ",
newFilters
);
const updatedUrlParams = filterTermsToQuerystring(
newFilters
);
if (newFilters.user === "Everyone") {
newFilters.user = undefined;
}
if (newFilters.title) {
newFilters.match = "W_CONTAINS";
}
if (newFilters.user === "Mine" && user) {
newFilters.user = user.uid;
}
setFilterTerms(newFilters);
history.push("?" + updatedUrlParams);
}}
/>
</Grid>
) : null}
</Grid>
<Paper elevation={3}>
{projectList ? (
<ProjectsTable
className={classes.table}
pageSizeOptions={[5, 10, 20]}
updateRequired={(page, pageSize, order, orderBy) => {
projectsForCommission(
commissionId,
page,
pageSize,
filterTerms,
order,
orderBy
)
.then(([projects, count]) => {
setProjectList(projects);
setProjectCount(count);
})
.catch((err) =>
console.error("Could not update project list: ", err)
);
}}
projects={projectList}
projectCount={projectCount}
user={user}
/>
) : null}
</Paper>
<Grid
container
direction="row"
justifyContent="space-between"
style={{ marginTop: "20px", marginBottom: "10px" }}
>
<Grid item>
<Typography variant="h4">Deliverables</Typography>
</Grid>
<Grid item>
<FormControl>
<InputLabel>Name Filter</InputLabel>
<Input onChange={(event) => stringUpdated(event)} />
</FormControl>
</Grid>
</Grid>
<Paper elevation={3}>
{commissionData ? (
<CommissionEntryDeliverablesComponent
commission={commissionData}
searchString={deliverablesSearchString}
/>
) : null}
</Paper>
<Dialog
open={errorDialog}
onClose={closeDialog}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogContent>
<DialogContentText id="alert-dialog-description">
The requested commission does not exist.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={closeDialog}>Close</Button>
</DialogActions>
</Dialog>
</>
) : (
<div>You have no access to this commission.</div>
)}
</>
);
};
export default CommissionEntryEditComponent;