Source: components/sla/SLAForm.es.js

/**
 * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
 *
 * The contents of this file are subject to the terms of the Liferay Enterprise
 * Subscription License ("License"). You may not use this file except in
 * compliance with the License. You can obtain a copy of the License by
 * contacting Liferay, Inc. See the License for the specific language governing
 * permissions and limitations under the License, including but not limited to
 * distribution rights of the Software.
 */

import {
	ALERT_MESSAGE,
	CALENDAR_KEY,
	DAYS,
	DURATION,
	HOURS,
	NAME,
	PAUSE_NODE_KEYS,
	START_NODE_KEYS,
	STOP_NODE_KEYS
} from './Constants.es';
import {AppContext, AppStatus} from '../AppContext.es';
import {
	BackLink,
	BackRedirect
} from '../../shared/components/router/routerWrapper.es';
import {Errors, useErrors} from './store/ErrorsStore.es';
import {
	hasErrors,
	validateDuration,
	validateHours,
	validateName,
	validateNodeKeys
} from './util/slaFormUtil.es';
import {SLA, useSLA} from './store/SLAStore.es';
import {SLANodes, useSLANodes} from './store/SLANodeStore.es';
import {useContext, useState} from 'react';
import calendarStore from './store/calendarStore.es';
import createNumberMask from 'text-mask-addons/dist/createNumberMask';
import FieldError from './form/fieldError.es';
import FieldLabel from './form/fieldLabel.es';
import Icon from '../../shared/components/Icon.es';
import LoadingState from '../../shared/components/loading/LoadingState.es';
import MaskedInput from 'react-text-mask';
import MultiSelect from '../../shared/components/MultiSelect.es';
import {openErrorToast} from '../../shared/util/toast.es';
import React from 'react';
import {Redirect} from 'react-router-dom';

/**
 * SLA form component.
 * @extends React.Component
 */
function SLAForm({id, processId, query}) {
	const {client} = useContext(AppContext);

	return (
		<div className="sla-form">
			<Errors.Provider value={useErrors()}>
				<SLANodes.Provider value={useSLANodes(processId, client)}>
					<SLA.Provider value={useSLA(client, id, processId)}>
						<SLAForm.Body
							id={id}
							processId={processId}
							query={query}
						/>
					</SLA.Provider>
				</SLANodes.Provider>
			</Errors.Provider>
		</div>
	);
}

const AlertMessage = () => {
	const {errors} = useContext(Errors);

	return (
		<div className="alert-container">
			<div className="alert alert-danger" role="alert">
				<span className="alert-indicator">
					<Icon iconName="exclamation-full" />
				</span>

				<strong className="lead">
					{Liferay.Language.get('error')}
				</strong>

				<span>{errors[ALERT_MESSAGE]}</span>
			</div>
		</div>
	);
};

const AlertWorkflowDefinitionChange = () => {
	return (
		<div className="alert-container">
			<div className="alert alert-danger alert-dismissible" role="alert">
				<span className="alert-indicator">
					<Icon iconName="exclamation-full" />
				</span>

				<strong className="lead">
					{Liferay.Language.get('error')}
				</strong>

				{Liferay.Language.get(
					'the-time-frame-options-changed-in-the-workflow-definition'
				)}

				<button
					aria-label="Close"
					className="close"
					data-dismiss="alert"
					type="button"
				>
					<Icon iconName="times" />
				</button>
			</div>
		</div>
	);
};

const Body = ({id, processId, query}) => {
	const {calendars} = calendarStore.getState();
	const {
		changeNodesKeys,
		changePauseNodes,
		changeValue,
		filterNodeTagIds,
		pauseNodeTagIds,
		sla: {
			calendarKey = calendarStore.defaultCalendar.key,
			days,
			description,
			hours,
			name,
			pauseNodeKeys: {nodeKeys: pauseNodeKeys},
			startNodeKeys: {nodeKeys: startNodeKeys},
			status,
			stopNodeKeys: {nodeKeys: stopNodeKeys}
		}
	} = useContext(SLA);
	const {errors, setErrors} = useContext(Errors);
	const {getPauseNodes, getStartNodes, getStopNodes, nodes} = useContext(
		SLANodes
	);
	const [loading, setLoading] = useState(false);

	const showCalendarField = calendars.length > 1;

	const daysMask = createNumberMask({
		allowLeadingZeroes: true,
		includeThousandsSeparator: false,
		integerLimit: 4,
		prefix: ''
	});

	const checkNodeErrors = type => filteredNodeKeys => {
		if (type !== PAUSE_NODE_KEYS) {
			errors[type] = validateNodeKeys(filteredNodeKeys);
		}
		errors[ALERT_MESSAGE] = '';
		setErrors({...errors});
	};

	const hideDropLists = () => () => {
		document.dispatchEvent(new Event('mousedown', {bubbles: true}));
	};

	const onChangeHandler = (saveFunc, validationFunc) => ({
		target: {name, value = ''}
	}) => {
		saveFunc(name, value);
		if (typeof validationFunc === 'function') {
			validationFunc(value);
		}
	};

	const onDurationChanged = (oldDays, oldHours) => days => {
		errors[ALERT_MESSAGE] = '';
		errors[DURATION] = validateDuration(days, oldHours);
		errors[HOURS] = '';

		setErrors({...errors});
	};

	const onHoursBlurred = (days, hours) => () => {
		if (days && Number(days) > 0 && (!hours || hours === '00:00')) {
			errors[ALERT_MESSAGE] = '';
			errors[HOURS] = '';
			setErrors({...errors});
		} else {
			const beforeHoursError = errors[HOURS];

			errors[ALERT_MESSAGE] = '';
			errors[HOURS] = validateHours(hours);

			if (hours && hours === '00:00') {
				errors[HOURS] = Liferay.Language.get(
					'value-must-be-an-hour-below'
				);
			}

			if (beforeHoursError !== errors[HOURS]) {
				setErrors({...errors});
			}
		}
	};

	const onNameChanged = name => {
		errors[ALERT_MESSAGE] = '';
		errors[NAME] = validateName(name);
		setErrors({...errors});
	};

	const onReloadNodesHandler = () => on => {
		setLoading(on);
	};

	let calendarString = Liferay.Language.get(
		'define-the-sla-duration-and-calendar-format'
	);

	if (!showCalendarField) {
		calendarString = Liferay.Language.get('define-the-sla-duration');
	}

	if (loading) {
		return <LoadingState />;
	}

	return (
		<>
			{errors[ALERT_MESSAGE] && <SLAForm.AlertMessage />}

			{status === 2 && <SLAForm.AlertWorkflowDefinitionChange />}

			<form className="sheet sheet-lg" role="form">
				<div className="sheet-header">
					<h2 className="sheet-title">
						{Liferay.Language.get('sla-definition')}
					</h2>
				</div>

				<div className="sheet-section">
					<div className="row">
						<div
							className={`col col-sm-5 form-group ${
								errors[NAME] ? 'has-error' : ''
							}`}
						>
							<FieldLabel
								fieldId="sla_name"
								required
								text={Liferay.Language.get('name')}
							/>

							<input
								className="form-control"
								defaultValue={name}
								id="sla_name"
								maxLength={75}
								name="name"
								onInput={onChangeHandler(
									changeValue,
									onNameChanged
								)}
								type="text"
							/>

							{errors[NAME] && (
								<FieldError error={errors[NAME]} />
							)}
						</div>

						<div className="col col-sm-7 form-group">
							<FieldLabel
								fieldId="sla_description"
								text={Liferay.Language.get('description')}
							/>

							<input
								className="form-control"
								defaultValue={description}
								id="sla_description"
								name="description"
								onFocus={hideDropLists()}
								onInput={onChangeHandler(changeValue)}
								type="text"
							/>
						</div>
					</div>

					<h3 className="sheet-subtitle">
						<FieldLabel
							fieldId="sla_time_start"
							text={Liferay.Language.get(
								'time-frame'
							).toUpperCase()}
						/>
					</h3>

					<div className="sheet-text">
						{Liferay.Language.get(
							'define-when-time-should-be-tracked-based-on-workflow-steps'
						)}
					</div>

					<div className="row">
						<div
							className={`col col-sm-12 form-group ${
								errors[START_NODE_KEYS] ? 'has-error' : ''
							}`}
						>
							<FieldLabel
								fieldId="sla_time_start"
								required
								text={Liferay.Language.get('start')}
							/>

							<div className="form-text">
								{Liferay.Language.get(
									'time-will-begin-counting-when'
								)}
							</div>

							<MultiSelect
								data={getStartNodes(
									pauseNodeKeys,
									stopNodeKeys
								)}
								fieldId="compositeId"
								onChangeTags={changeNodesKeys(
									START_NODE_KEYS,
									nodes,
									checkNodeErrors(START_NODE_KEYS)
								)}
								selectedTagsId={filterNodeTagIds(
									getStartNodes(pauseNodeKeys, stopNodeKeys),
									startNodeKeys
								)}
							/>

							{errors[START_NODE_KEYS] && (
								<FieldError error={errors[START_NODE_KEYS]} />
							)}
						</div>
					</div>

					<div className="row">
						<div className="col col-sm-12 form-group">
							<FieldLabel
								fieldId="sla_time_pause"
								text={Liferay.Language.get('pause')}
							/>

							<div className="form-text">
								{Liferay.Language.get(
									'time-wont-be-considered-when'
								)}
							</div>

							<MultiSelect
								data={getPauseNodes(
									startNodeKeys,
									stopNodeKeys
								)}
								fieldId="compositeId"
								onChangeTags={changePauseNodes(
									getPauseNodes(startNodeKeys, stopNodeKeys),
									checkNodeErrors(PAUSE_NODE_KEYS)
								)}
								selectedTagsId={pauseNodeTagIds(
									getPauseNodes(startNodeKeys, stopNodeKeys),
									pauseNodeKeys
								)}
							/>
						</div>
					</div>

					<div className="row">
						<div
							className={`col col-sm-12 form-group ${
								errors[STOP_NODE_KEYS] ? 'has-error' : ''
							}`}
						>
							<FieldLabel
								fieldId="sla_time_stop"
								required
								text={Liferay.Language.get('stop')}
							/>

							<div className="form-text">
								{Liferay.Language.get(
									'time-will-stop-counting-when'
								)}
							</div>

							<MultiSelect
								data={getStopNodes(
									pauseNodeKeys,
									startNodeKeys
								)}
								fieldId="compositeId"
								onChangeTags={changeNodesKeys(
									STOP_NODE_KEYS,
									nodes,
									checkNodeErrors(STOP_NODE_KEYS)
								)}
								selectedTagsId={filterNodeTagIds(
									getStopNodes(pauseNodeKeys, startNodeKeys),
									stopNodeKeys
								)}
							/>

							{errors[STOP_NODE_KEYS] && (
								<FieldError error={errors[STOP_NODE_KEYS]} />
							)}
						</div>
					</div>

					<h3 className="sheet-subtitle">
						<FieldLabel
							required
							text={Liferay.Language.get(
								'duration'
							).toUpperCase()}
						/>
					</h3>

					<div className="sheet-text">{calendarString}</div>

					<div className="row">
						<div
							className={`col col-sm-3 form-group ${
								errors[DURATION] ? 'has-error' : ''
							}`}
						>
							<FieldLabel
								fieldId="sla_duration_days"
								text={Liferay.Language.get('days')}
							/>

							<MaskedInput
								className="form-control"
								defaultValue={days}
								id="sla_duration_days"
								mask={daysMask}
								maxLength={4}
								name={DAYS}
								onFocus={hideDropLists()}
								onInput={onChangeHandler(
									changeValue,
									onDurationChanged(days, hours)
								)}
							/>

							{errors[DURATION] && (
								<FieldError error={errors[DURATION]} />
							)}

							<div className="form-text">
								{Liferay.Language.get('enter-a-whole-number')}
							</div>
						</div>

						<div
							className={`col col-sm-3 form-group ${
								errors[DURATION] || errors[HOURS]
									? 'has-error'
									: ''
							}`}
						>
							<FieldLabel
								fieldId="sla_duration_hours"
								text={Liferay.Language.get('hours')}
							/>

							<MaskedInput
								className="form-control"
								id="sla_duration_hours"
								mask={[/\d/, /\d/, ':', /\d/, /\d/]}
								name={HOURS}
								onBlur={onHoursBlurred(days, hours)}
								onChange={onChangeHandler(
									changeValue,
									onDurationChanged(days, hours)
								)}
								onInput={onChangeHandler(changeValue)}
								placeholder="00:00"
								value={hours}
							/>

							{errors[DURATION] && (
								<FieldError error={errors[DURATION]} />
							)}

							{errors[HOURS] && (
								<FieldError error={errors[HOURS]} />
							)}
						</div>

						{showCalendarField && (
							<div className="col col-sm-6 form-group">
								<FieldLabel
									fieldId="sla_calendar_key"
									text={Liferay.Language.get('calendar')}
								/>

								<select
									className="form-control"
									id="sla_calendar_key"
									name={CALENDAR_KEY}
									onChange={onChangeHandler(changeValue)}
									value={calendarKey}
								>
									{calendars.map((calendar, index) => (
										<option
											key={index}
											value={calendar.key}
										>
											{calendar.title}

											{calendar.defaultCalendar &&
												` (${Liferay.Language.get(
													'system-default'
												)})`}
										</option>
									))}
								</select>
							</div>
						)}
					</div>
				</div>

				<SLAForm.Footer
					id={id}
					onReloadNodes={onReloadNodesHandler()}
					processId={processId}
					query={query}
				/>
			</form>
		</>
	);
};

const Footer = ({id, onReloadNodes, processId, query}) => {
	const {defaultDelta, setStatus} = useContext(AppContext);
	const {errors, setErrors} = useContext(Errors);
	const {fetchNodes} = useContext(SLANodes);
	const [redirectToSLAList, setRedirectToSLAList] = useState(false);
	const {resetNodes, saveSLA, sla} = useContext(SLA);

	const handleErrors = responseErrors => {
		if (Array.isArray(responseErrors)) {
			responseErrors.forEach(error => {
				const errorKey = error.fieldName || ALERT_MESSAGE;

				errors[errorKey] = error.message;
			});

			const fieldNames = responseErrors.map(error => error.fieldName);

			if (
				fieldNames.includes(PAUSE_NODE_KEYS) ||
				fieldNames.includes(START_NODE_KEYS) ||
				fieldNames.includes(STOP_NODE_KEYS)
			) {
				resetNodes();
				onReloadNodes(true);
				fetchNodes(processId).then(() => onReloadNodes(false));
				setErrors({...errors});
			} else {
				setErrors({...errors});
			}
		} else {
			openErrorToast({
				message: Liferay.Language.get(
					'there-was-a-problem-retrieving-data-please-try-reloading-the-page'
				)
			});
		}
	};

	const handleSubmit = (
		{days, hours, id, name, processId, startNodeKeys, stopNodeKeys},
		saveCallback
	) => () => {
		errors[ALERT_MESSAGE] = '';
		errors[DURATION] = validateDuration(days, hours);
		errors[HOURS] = validateHours(hours);
		errors[NAME] = validateName(name);
		errors[PAUSE_NODE_KEYS] = '';
		errors[START_NODE_KEYS] = validateNodeKeys(startNodeKeys.nodeKeys);
		errors[STOP_NODE_KEYS] = validateNodeKeys(stopNodeKeys.nodeKeys);

		if ((!hours || hours === '00:00') && (days && Number(days) > 0)) {
			errors[HOURS] = '';
		}

		if (hasErrors(errors)) {
			errors[ALERT_MESSAGE] = Liferay.Language.get(
				'please-fill-in-the-required-fields'
			);
			setErrors({...errors});
		} else {
			saveCallback(processId, id, calendarStore.defaultCalendar.key)
				.then(() => {
					const status = id
						? AppStatus.slaUpdated
						: AppStatus.slaSaved;

					setStatus(status, () => {
						setRedirectToSLAList(true);
					});
				})
				.catch(result => {
					const {
						response: {data}
					} = result;

					handleErrors(data);
				});
		}
	};

	return (
		<>
			{redirectToSLAList && id ? (
				<BackRedirect />
			) : redirectToSLAList ? (
				<Redirect
					to={{
						pathname: `/slas/${processId}/${defaultDelta}/1`,
						search: query
					}}
				/>
			) : null}
			<div className="sheet-footer sheet-footer-btn-block-sm-down">
				<div className="btn-group">
					<div className="btn-group-item">
						<button
							className="btn btn-primary"
							onClick={handleSubmit(
								{...sla, ...{id, processId}},
								saveSLA
							)}
							type="button"
						>
							{id
								? Liferay.Language.get('update')
								: Liferay.Language.get('save')}
						</button>
					</div>

					<div className="btn-group-item">
						<BackLink className="btn btn-secondary">
							{Liferay.Language.get('cancel')}
						</BackLink>
					</div>
				</div>
			</div>
		</>
	);
};

SLAForm.AlertMessage = AlertMessage;
SLAForm.AlertWorkflowDefinitionChange = AlertWorkflowDefinitionChange;
SLAForm.Body = Body;
SLAForm.Footer = Footer;
export default SLAForm;