From 23bd230a1d2835a658b4c4279a68a9e5554755d1 Mon Sep 17 00:00:00 2001 From: Martin Krulis Date: Wed, 29 Dec 2021 02:14:21 +0100 Subject: [PATCH] Finalizing pipeline submission (making global adjustments to submit button and submit error reporting). --- .../Pipelines/BoxesTable/BoxesTable.js | 11 +- .../Pipelines/BoxesTable/BoxesTableRow.js | 12 +- .../Pipelines/PipelineGraph/PipelineGraph.js | 20 +- .../VariablesTable/VariablesTable.js | 14 +- .../EditPipelineForm/EditPipelineForm.js | 7 - .../forms/SubmitButton/SubmitButton.js | 98 ++++++-- src/components/widgets/TheButton/TheButton.js | 64 +++--- .../PipelineEditContainer.js | 217 ++++++++++++++---- src/helpers/common.js | 21 +- src/helpers/pipelines.js | 18 +- src/locales/apiErrorMessages.js | 5 + src/locales/cs.json | 5 + src/locales/en.json | 5 + src/redux/modules/pipelines.js | 1 + 14 files changed, 367 insertions(+), 131 deletions(-) diff --git a/src/components/Pipelines/BoxesTable/BoxesTable.js b/src/components/Pipelines/BoxesTable/BoxesTable.js index a4641bc84..6c3120380 100644 --- a/src/components/Pipelines/BoxesTable/BoxesTable.js +++ b/src/components/Pipelines/BoxesTable/BoxesTable.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { injectIntl, FormattedMessage } from 'react-intl'; import { Table } from 'react-bootstrap'; import { defaultMemoize } from 'reselect'; +import classnames from 'classnames'; import BoxesTableRow from './BoxesTableRow'; import { arrayToObject } from '../../../helpers/common'; @@ -23,13 +24,19 @@ const BoxesTable = ({ secondarySelections = null, selectedVariable = null, removeBox = null, + pending = false, intl: { locale }, ...rowProps }) => { const selectionIndex = secondarySelections && prepareSelectionIndex(secondarySelections); const variable = selectedVariable && variables && variables.find(v => v.name === selectedVariable); return ( - 0 ? 'tbody-hover' : ''} size="sm"> +
0 && !pending, + })} + size="sm"> selectBox(box.name) : null} - onDoubleClick={editBox ? () => editBox(box.name) : null} + onClick={selectBox && !pending ? () => selectBox(box.name) : null} + onDoubleClick={editBox && !pending ? () => editBox(box.name) : null} className={classnames({ - clickable: editBox || selectBox, + clickable: (editBox || selectBox) && !pending, [styles.primarySelection]: primarySelection === box.name, [styles.secondarySelection]: secondarySelections && secondarySelections[box.name], })}> @@ -282,7 +283,9 @@ const BoxesTableRow = ({ timid onClick={ev => { ev.stopPropagation(); - removeBox(box.name); + if (!pending) { + removeBox(box.name); + } }} /> @@ -314,6 +317,7 @@ BoxesTableRow.propTypes = { editBox: PropTypes.func, removeBox: PropTypes.func, assignVariable: PropTypes.func, + pending: PropTypes.bool, }; export default BoxesTableRow; diff --git a/src/components/Pipelines/PipelineGraph/PipelineGraph.js b/src/components/Pipelines/PipelineGraph/PipelineGraph.js index e4fe1f742..74ac84963 100644 --- a/src/components/Pipelines/PipelineGraph/PipelineGraph.js +++ b/src/components/Pipelines/PipelineGraph/PipelineGraph.js @@ -250,6 +250,7 @@ const PipelineGraph = ({ editBox = null, selectVariable = null, editVariable = null, + pending = false, }) => { if (canUseDOM) { const [svg, setSvg] = useState(null); @@ -271,7 +272,7 @@ const PipelineGraph = ({ }, [boxes, variables, utilization, selectedBox, selectedVariable]); return ( - + {canUseDOM && svg ? (
{ ev.preventDefault(); - const { box, variable } = preprocessClickEvent(ev, boxIds, variableIds); - box && selectBox && selectBox(box); - variable && selectVariable && selectVariable(variable); + if (!pending) { + const { box, variable } = preprocessClickEvent(ev, boxIds, variableIds); + box && selectBox && selectBox(box); + variable && selectVariable && selectVariable(variable); + } }} onClick={ev => { - const { box, variable } = preprocessClickEvent(ev, boxIds, variableIds); - box && editBox && editBox(box); - variable && editVariable && editVariable(variable); + if (!pending) { + const { box, variable } = preprocessClickEvent(ev, boxIds, variableIds); + box && editBox && editBox(box); + variable && editVariable && editVariable(variable); + } }} /> ) : ( @@ -312,6 +317,7 @@ PipelineGraph.propTypes = { editBox: PropTypes.func, selectVariable: PropTypes.func, editVariable: PropTypes.func, + pending: PropTypes.bool, }; export default PipelineGraph; diff --git a/src/components/Pipelines/VariablesTable/VariablesTable.js b/src/components/Pipelines/VariablesTable/VariablesTable.js index fe7617e1e..8cd6a161d 100644 --- a/src/components/Pipelines/VariablesTable/VariablesTable.js +++ b/src/components/Pipelines/VariablesTable/VariablesTable.js @@ -28,12 +28,13 @@ const VariablesTable = ({ selectVariable = null, editVariable = null, removeVariable = null, + pending = false, intl: { locale }, }) => { const secondarySelectionsIndexed = secondarySelections && prepareSelectionIndex(secondarySelections); return ( -
@@ -62,6 +69,7 @@ const BoxesTable = ({ selectedVariable={variable} secondarySelections={selectionIndex} removeBox={removeBox} + pending={pending} {...rowProps} /> ))} @@ -90,6 +98,7 @@ BoxesTable.propTypes = { removeBox: PropTypes.func, secondarySelections: PropTypes.array, selectedVariable: PropTypes.string, + pending: PropTypes.bool, intl: PropTypes.shape({ locale: PropTypes.string.isRequired }).isRequired, }; diff --git a/src/components/Pipelines/BoxesTable/BoxesTableRow.js b/src/components/Pipelines/BoxesTable/BoxesTableRow.js index d589c4e60..f62f3cff9 100644 --- a/src/components/Pipelines/BoxesTable/BoxesTableRow.js +++ b/src/components/Pipelines/BoxesTable/BoxesTableRow.js @@ -219,6 +219,7 @@ const BoxesTableRow = ({ editBox = null, removeBox = null, assignVariable = null, + pending = false, }) => { const [firstPort, ...ports] = [ ...transformPorts(box, 'portsIn', boxTypes), @@ -229,10 +230,10 @@ const BoxesTableRow = ({ return (
0} size="sm"> +
0 && !pending} className={pending ? 'half-opaque' : ''} size="sm"> {utilization && selectVariable(variable.name) : null} - onDoubleClick={editVariable ? () => editVariable(variable.name) : null} + onClick={selectVariable && !pending ? () => selectVariable(variable.name) : null} + onDoubleClick={editVariable && !pending ? () => editVariable(variable.name) : null} className={classnames({ - clickable: selectVariable || editVariable, + clickable: (selectVariable || editVariable) && !pending, [styles.primarySelection]: primarySelection === variable.name, [styles.secondarySelection]: secondarySelectionsIndexed && secondarySelectionsIndexed[variable.name], })}> @@ -158,7 +159,9 @@ const VariablesTable = ({ timid onClick={ev => { ev.stopPropagation(); - removeVariable(variable.name); + if (!pending) { + removeVariable(variable.name); + } }} /> @@ -192,6 +195,7 @@ VariablesTable.propTypes = { selectVariable: PropTypes.func, editVariable: PropTypes.func, removeVariable: PropTypes.func, + pending: PropTypes.bool, intl: PropTypes.shape({ locale: PropTypes.string.isRequired }).isRequired, }; diff --git a/src/components/forms/EditPipelineForm/EditPipelineForm.js b/src/components/forms/EditPipelineForm/EditPipelineForm.js index 99887d1a0..ac6c201da 100644 --- a/src/components/forms/EditPipelineForm/EditPipelineForm.js +++ b/src/components/forms/EditPipelineForm/EditPipelineForm.js @@ -6,7 +6,6 @@ import { Container, Row, Col } from 'react-bootstrap'; import { TextField, MarkdownTextAreaField, CheckboxField } from '../Fields'; import { SaveIcon } from '../../icons'; -import Callout from '../../widgets/Callout'; import FormBox from '../../widgets/FormBox'; import SubmitButton from '../SubmitButton'; @@ -45,12 +44,6 @@ class EditPipelineForm extends Component { /> }> - {submitFailed && ( - - - - )} - diff --git a/src/components/forms/SubmitButton/SubmitButton.js b/src/components/forms/SubmitButton/SubmitButton.js index 4803334c8..713c6f3a6 100644 --- a/src/components/forms/SubmitButton/SubmitButton.js +++ b/src/components/forms/SubmitButton/SubmitButton.js @@ -1,11 +1,13 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, injectIntl } from 'react-intl'; +import { Popover, OverlayTrigger } from 'react-bootstrap'; import { defaultMemoize } from 'reselect'; import Button from '../../widgets/TheButton'; import { SendIcon, LoadingIcon, SuccessIcon, WarningIcon } from '../../icons'; import Confirm from '../Confirm'; +import { getErrorMessage } from '../../../locales/apiErrorMessages'; const getIcons = defaultMemoize(defaultIcon => ({ submit: defaultIcon || , @@ -33,7 +35,7 @@ const getMessages = defaultMemoize(messages => { }); class SubmitButton extends Component { - state = { saved: false }; + state = { saved: false, lastError: null }; componentWillUnmount() { this.unmounted = true; @@ -46,22 +48,32 @@ class SubmitButton extends Component { submit = () => { const { handleSubmit, onSubmit = null } = this.props; + + // reset button internal state + this.setState({ saved: false, lastError: null }); + onSubmit && onSubmit(); - return handleSubmit().then(res => { - if (!this.unmounted) { - this.setState({ saved: true }); - this.resetAfterSomeTime = setTimeout(this.reset, 2000); - } else { - const { reset } = this.props; - reset && reset(); // the redux form must be still reset + + return handleSubmit().then( + res => { + if (!this.unmounted) { + this.setState({ saved: true }); + this.resetAfterSomeTime = setTimeout(this.reset, 2000); + } else { + const { reset } = this.props; + reset && reset(); // the redux form must be still reset + } + return res; + }, + lastError => { + this.setState({ lastError }); } - return res; - }); + ); }; reset = () => { const { reset } = this.props; - this.setState({ saved: false }); + this.setState({ saved: false, lastError: null }); reset && reset(); }; @@ -74,32 +86,68 @@ class SubmitButton extends Component { return 'submit'; }; + getButtonVariant() { + const { submitting, hasFailed, invalid } = this.props; + return hasFailed && !submitting + ? 'danger' + : this.state.saved || submitting + ? 'success' + : invalid + ? 'warning' + : 'success'; + } + + isButtonDisabled() { + const { submitting, invalid, asyncValidating = false, disabled = false } = this.props; + return invalid || asyncValidating !== false || submitting || disabled; + } + render() { const { - submitting, + id, hasFailed, - invalid, - asyncValidating = false, + confirmQuestion = '', noIcons = false, defaultIcon = null, - disabled = false, - confirmQuestion = '', noShadow = false, messages = {}, + intl: { formatMessage }, } = this.props; - const { saved: hasSucceeded } = this.state; const buttonState = this.getButtonState(); const icons = getIcons(defaultIcon); const formattedMessages = getMessages(messages); - return ( - + return hasFailed && this.state.lastError ? ( + + + + + + {getErrorMessage(formatMessage)(this.state.lastError)} + + + }> + + ) : ( + + @@ -112,7 +160,6 @@ SubmitButton.propTypes = { id: PropTypes.string.isRequired, handleSubmit: PropTypes.func.isRequired, submitting: PropTypes.bool, - hasSucceeded: PropTypes.bool, hasFailed: PropTypes.bool, asyncValidating: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), noIcons: PropTypes.bool, @@ -129,6 +176,7 @@ SubmitButton.propTypes = { }), confirmQuestion: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), noShadow: PropTypes.bool, + intl: PropTypes.object, }; -export default SubmitButton; +export default injectIntl(SubmitButton); diff --git a/src/components/widgets/TheButton/TheButton.js b/src/components/widgets/TheButton/TheButton.js index 0cdb32571..6bcf64d0a 100644 --- a/src/components/widgets/TheButton/TheButton.js +++ b/src/components/widgets/TheButton/TheButton.js @@ -4,36 +4,44 @@ import { Button } from 'react-bootstrap'; import classnames from 'classnames'; import Confirm from '../../forms/Confirm'; -const TheButtonInternal = ({ className, onClick = null, variant, noShadow = false, ...props }) => ( - - - - - - - - + 0)} + defaultIcon={} + messages={{ + success: , + submit: , + submitting: , + invalid: , + }} + /> ) }> <> + {this.state.version < pipeline.version && ( + +

+ +

+

+ +

+ + + + + +
+ )} + {this.state.pipelineStructureCoerced ? ( <> @@ -787,6 +893,7 @@ class PipelineEditContainer extends Component { editBox={this.openBoxForm} removeBox={this.removeBox} assignVariable={this.assignVariable} + pending={this.state.submitting || this.state.version < pipeline.version} /> )} @@ -804,6 +911,7 @@ class PipelineEditContainer extends Component { selectVariable={this.selectVariable} editVariable={this.openVariableForm} removeVariable={this.removeVariable} + pending={this.state.submitting || this.state.version < pipeline.version} /> )} @@ -823,6 +931,7 @@ class PipelineEditContainer extends Component { editBox={this.openBoxForm} selectVariable={this.selectVariable} editVariable={this.openVariableForm} + pending={this.state.submitting || this.state.version < pipeline.version} /> @@ -888,6 +997,8 @@ PipelineEditContainer.propTypes = { }).isRequired, supplementaryFiles: ImmutablePropTypes.map, boxTypes: PropTypes.object.isRequired, + editPipeline: PropTypes.func.isRequired, + reloadPipeline: PropTypes.func.isRequired, intl: PropTypes.object, }; @@ -898,6 +1009,16 @@ export default connect( }; }, (dispatch, { pipeline }) => ({ - // + editPipeline: pipelineStructure => + dispatch( + editPipeline(pipeline.id, { + name: pipeline.name, + description: pipeline.description, + version: pipeline.version, + global: pipeline.author === null, + pipeline: pipelineStructure, + }) + ), + reloadPipeline: () => dispatch(reloadPipeline(pipeline.id)), }) )(injectIntl(PipelineEditContainer)); diff --git a/src/helpers/common.js b/src/helpers/common.js index ddfe64a3e..cb05dc15d 100644 --- a/src/helpers/common.js +++ b/src/helpers/common.js @@ -203,23 +203,34 @@ export const getFirstItemInOrder = (arr, comarator = _defaultComparator) => { * the items/properties are compared recursively. * @param {*} a * @param {*} b + * @param {boolean} emptyObjectArrayEquals if true, {} and [] are treated as equal * @returns {boolean} true if the values are matching */ -export const deepCompare = (a, b) => { - if (typeof a !== typeof b || Array.isArray(a) !== Array.isArray(b)) { +export const deepCompare = (a, b, emptyObjectArrayEquals = false) => { + if (typeof a !== typeof b) { return false; } - if (typeof a !== 'object') { + + if (typeof a !== 'object' || a === null || b === null) { return a === b; // compare scalars + } else if (emptyObjectArrayEquals && Object.keys(a).length === 0 && Object.keys(b).length === 0) { + return true; // special case, empty objects are compared regardless of their prototype } + + if (Array.isArray(a) !== Array.isArray(b)) { + return false; + } + if (Array.isArray(a)) { // compare arrays - return a.length === b.length ? a.every((val, idx) => deepCompare(val, b[idx])) : false; + return a.length === b.length ? a.every((val, idx) => deepCompare(val, b[idx], emptyObjectArrayEquals)) : false; } else { // compare objects const aKeys = Object.keys(a); const bKeys = new Set(Object.keys(b)); - return aKeys.length === bKeys.size ? aKeys.every(key => bKeys.has(key) && deepCompare(a[key], b[key])) : false; + return aKeys.length === bKeys.size + ? aKeys.every(key => bKeys.has(key) && deepCompare(a[key], b[key], emptyObjectArrayEquals)) + : false; } }; diff --git a/src/helpers/pipelines.js b/src/helpers/pipelines.js index fd949bdb3..81ffd913b 100644 --- a/src/helpers/pipelines.js +++ b/src/helpers/pipelines.js @@ -4,7 +4,7 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import { defaultMemoize } from 'reselect'; -import { arrayToObject, identity, objectFilter } from './common'; +import { arrayToObject, identity, objectFilter, deepCompare } from './common'; export const KNOWN_DATA_TYPES = ['file', 'remote-file', 'string'] .reduce((acc, type) => [...acc, type, type + '[]'], []) @@ -107,6 +107,22 @@ export const getVariablesTypes = defaultMemoize(variables => ) ); +const nameTypeComparator = (a, b) => + (a.name || '').localeCompare(b.name || '') || (a.type || '').localeCompare(b.type || ''); + +export const comparePipelineEntities = (entities1, entities2) => { + if (!Array.isArray(entities1) || !Array.isArray(entities2)) { + return deepCompare(entities1, entities2, true); + } + if (entities1.length !== entities2.length) { + return false; + } + + const sorted1 = [...entities1].sort(nameTypeComparator); + const sorted2 = [...entities2].sort(nameTypeComparator); + return deepCompare(sorted1, sorted2, true); +}; + /* * Structural checks */ diff --git a/src/locales/apiErrorMessages.js b/src/locales/apiErrorMessages.js index 945cca93f..0237f49b9 100644 --- a/src/locales/apiErrorMessages.js +++ b/src/locales/apiErrorMessages.js @@ -15,6 +15,11 @@ const apiErrorCodes = defineMessages({ id: 'app.apiErrorCodes.400-004', defaultMessage: 'Uploaded file size does not meet server limitations.', }, + '400-010': { + id: 'app.apiErrorCodes.400-010', + defaultMessage: + 'The data were modified by someone else while you were editing them (a newer version exist). The update was aborted to prevent accidental overwrite of recent modifications.', + }, '400-101': { id: 'app.apiErrorCodes.400-101', defaultMessage: 'The credentials are not valid.' }, '400-104': { id: 'app.apiErrorCodes.400-104', diff --git a/src/locales/cs.json b/src/locales/cs.json index 808fe5659..cd5de5d7c 100644 --- a/src/locales/cs.json +++ b/src/locales/cs.json @@ -44,6 +44,7 @@ "app.apiErrorCodes.400-001": "Uživatel má více registrovaných e-malových adres, které odpovídají více než jednomu existujícímu účtu. Asociace účtů není možná z důvodu nejednoznačnosti.", "app.apiErrorCodes.400-003": "Název nahrávaného souboru obsahuje nepovolené znaky.", "app.apiErrorCodes.400-004": "Nahrávaný soubor nesplňuje omezení serveru na velikost.", + "app.apiErrorCodes.400-010": "Během vaší editace byla data změněna někým jiným (existuje novější verze). Uložení dat nebylo provedeno, aby se zabránilo náhodnému přepsání naposledy uložených změn.", "app.apiErrorCodes.400-101": "Nesprávné přihlašovací údaje.", "app.apiErrorCodes.400-104": "Uživatel byl korektně ověřen, ale nemá žádný odpovídající účet v ReCodExu.", "app.apiErrorCodes.400-105": "Pokus o zavedení nového účtu selhal, protože externí autentizační služba nedodala uživatelskou roli. Externí uživatelská identita pravděpodobně nemá potřebné atributy.", @@ -1127,6 +1128,8 @@ "app.pipelineEditContainer.structureCoercedWarningTitle": "Struktura pipeline je rozbitá", "app.pipelineEditContainer.title": "Struktura pipeline", "app.pipelineEditContainer.variablesTitle": "Proměnné", + "app.pipelineEditContainer.versionChanged": "Původní stuktura pipeline byla změněna v průběhu provádění vašich změn. Pokud necháte znovu načíst pipeline, zůstane současná verze v historii (takže můžete použít tlačítko zpět a vrátit se k vámi rozpracované verzi).", + "app.pipelineEditContainer.versionChangedTitle": "Pipeline byla změněna", "app.pipelineFilesTable.description": "Testovací soubory jsou soubory, které mohou být odkazované v konfiguraci pipeline.", "app.pipelineFilesTable.title": "Soubory pipeline", "app.pipelineParams.hasEntryPoint": "Testované řešení by mělo specifikovat vstupní bod aplikace", @@ -1632,6 +1635,7 @@ "app.submistSolution.instructions": "Musíte připojit alespoň jeden soubor se zdrojovým kódem a počkat, než jsou všechny soubory nahrány na server. Pokud nastane problém při uploadu některých soborů, nahrajte je znovu nebo soubory odeberte. Jméno souboru NESMÍ OBSAHOVAT žádné nestandardní znaky (například v kódování UTF-8).", "app.submistSolution.submitFailed": "Odevzdané řešení bylo aktivně odmítnuto. Toto je obvykle způsobeno nahráním souborů se jménem obsahujícím nestandardní znaky nebo špatnou koncovkou. Pokud nemůžete odevzdat řešení bez zjevného důvodu, kontaktujte prosím svého vyučujícího.", "app.submitButton.invalid": "Nějaký vstup není validní", + "app.submitButton.lastError.title": "Nastala chyba", "app.submitRefSolution.noteLabel": "Popis referenčního řešení", "app.submitRefSolution.title": "Vytvořit referenční řešení", "app.submitSolution.emptyNoteSubmitConfirm": "Popis referenčního řešení je prázdný. Je nanejvýš vhodné, aby referenční řešení byla opatřena relevantním popiskem. Opravdu si přejete pokračovat v odevzdání?", @@ -1755,6 +1759,7 @@ "generic.inUse": "používá se", "generic.invertSelection": "Invertovat výběr", "generic.lastUpdatedAt": "aktualizováno", + "generic.load": "Načíst", "generic.loading": "Načítání...", "generic.name": "Název", "generic.nameOfPerson": "Jméno", diff --git a/src/locales/en.json b/src/locales/en.json index d079cb861..d44adb6f0 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -44,6 +44,7 @@ "app.apiErrorCodes.400-001": "The user has multiple e-mail addresses and multiple matching accounts already exist. Accounts cannot be associated due to ambiguity.", "app.apiErrorCodes.400-003": "Uploaded file name contains invalid characters.", "app.apiErrorCodes.400-004": "Uploaded file size does not meet server limitations.", + "app.apiErrorCodes.400-010": "The data were modified by someone else while you were editing them (a newer version exist). The update was aborted to prevent accidental overwrite of recent modifications.", "app.apiErrorCodes.400-101": "The credentials are not valid.", "app.apiErrorCodes.400-104": "User was correctly authenticated, but there is no corresponding account in ReCodEx.", "app.apiErrorCodes.400-105": "Attempt to register a new user failed since external authenticator did not provide a role. The external user identity may not have required attributes.", @@ -1127,6 +1128,8 @@ "app.pipelineEditContainer.structureCoercedWarningTitle": "The pipeline structure was broken", "app.pipelineEditContainer.title": "Edit Pipeline Structure", "app.pipelineEditContainer.variablesTitle": "Variables", + "app.pipelineEditContainer.versionChanged": "The pipeline structure was updated whilst you were editing it. If you load the new pipeline, it will be pushed as a new state in editor (you can use undo button to revert it).", + "app.pipelineEditContainer.versionChangedTitle": "The pipeline was updated", "app.pipelineFilesTable.description": "Supplementary files are files which can be referenced as remote file in pipeline configuration.", "app.pipelineFilesTable.title": "Pipeline files", "app.pipelineParams.hasEntryPoint": "Tested solution is expected to specify entry point of the application", @@ -1632,6 +1635,7 @@ "app.submistSolution.instructions": "You must attach at least one file with source code and wait, until all your files are uploaded to the server. If there is a problem uploading any of the files, check the name of the file. The name MUST NOT contain non-standard characters (like UTF-8 ones). Then try to upload it again.", "app.submistSolution.submitFailed": "Submission was rejected by the server. This usually means you have uploaded incorrect files - do your files have name with ASCII characters only and proper file type extensions? If you cannot submit the solution and there is no obvious reason, contact your supervisor to sort things out.", "app.submitButton.invalid": "Some input is invalid.", + "app.submitButton.lastError.title": "An error occured", "app.submitRefSolution.noteLabel": "Description of the reference solution:", "app.submitRefSolution.title": "Create Reference Solution", "app.submitSolution.emptyNoteSubmitConfirm": "The description is empty. Reference solutions are strongly encouraged to be labeled with relevant descriptions. Do you rellay wish to proceed with submit?", @@ -1755,6 +1759,7 @@ "generic.inUse": "in use", "generic.invertSelection": "Invert Selection", "generic.lastUpdatedAt": "updated", + "generic.load": "Load", "generic.loading": "Loading...", "generic.name": "Name", "generic.nameOfPerson": "Name", diff --git a/src/redux/modules/pipelines.js b/src/redux/modules/pipelines.js index 59c1ece15..95c3170f9 100644 --- a/src/redux/modules/pipelines.js +++ b/src/redux/modules/pipelines.js @@ -24,6 +24,7 @@ export const additionalActionTypes = { export const fetchPipeline = actions.fetchResource; export const fetchPipelineIfNeeded = actions.fetchOneIfNeeded; +export const reloadPipeline = (id, meta = {}) => actions.fetchResource(id, { allowReload: true, ...meta }); export const fetchManyEndpoint = '/pipelines';
} @@ -58,10 +59,10 @@ const VariablesTable = ({ return (