diff --git a/client/src/components/Applications/AppForm/AppForm.jsx b/client/src/components/Applications/AppForm/AppForm.jsx index 40edc6b9d..17b794dbf 100644 --- a/client/src/components/Applications/AppForm/AppForm.jsx +++ b/client/src/components/Applications/AppForm/AppForm.jsx @@ -19,10 +19,13 @@ import { Link } from 'react-router-dom'; import { getSystemName } from 'utils/systems'; import FormSchema from './AppFormSchema'; import { + isTargetPathField, + getInputFieldFromTargetPathField, getQueueMaxMinutes, getMaxMinutesValidation, getNodeCountValidation, getCoresPerNodeValidation, + getTargetPathFieldName, updateValuesForQueue, } from './AppFormUtils'; import DataFilesSelectModal from '../../DataFiles/DataFilesModals/DataFilesSelectModal'; @@ -459,17 +462,30 @@ export const AppSchemaForm = ({ app }) => { onSubmit={(values, { setSubmitting, resetForm }) => { const job = cloneDeep(values); - job.fileInputs = Object.entries(job.fileInputs) - .map(([k, v]) => { - return { - name: k, - sourceUrl: v, - targetPath: app.definition.jobAttributes.fileInputs.find( - (file) => file.name === k - ).targetPath, - }; - }) - .filter((fileInput) => fileInput.sourceUrl); // filter out any empty values + // Tranform input field values into format that jobs service wants + job.fileInputs = Object.values( + Object.entries(job.fileInputs) + .map(([k, v]) => { + return { + name: k, + sourceUrl: !isTargetPathField(k) ? v : null, + targetDir: isTargetPathField(k) ? v : null, + }; + }) + .filter((fileInput) => fileInput.sourceUrl || fileInput.targetDir) // filter out any empty values + .reduce((acc, entry) => { // merge input field and targetPath fields into one. + const key = getInputFieldFromTargetPathField(entry.name); + if (!acc[key]) { + acc[key] = {}; + } + acc[key]['name'] = key; + acc[key]['sourceUrl'] = + acc[key]['sourceUrl'] ?? entry.sourceUrl; + acc[key]['targetPath'] = + acc[key]['targetPath'] ?? entry.targetDir; + return acc; + }, {}) + ).flat(); job.parameterSet = Object.assign( {}, @@ -550,16 +566,23 @@ export const AppSchemaForm = ({ app }) => { {Object.entries(appFields.fileInputs).map( ([name, field]) => { - // TODOv3 handle fileInputArrays https://jira.tacc.utexas.edu/browse/TV3-81 + // TODOv3 handle fileInputArrays https://jira.tacc.utexas.edu/browse/WP-81 return ( - + isTargetPathField(name)? + : + ); } )} diff --git a/client/src/components/Applications/AppForm/AppFormSchema.js b/client/src/components/Applications/AppForm/AppFormSchema.js index ab9a98d09..310b61c4a 100644 --- a/client/src/components/Applications/AppForm/AppFormSchema.js +++ b/client/src/components/Applications/AppForm/AppFormSchema.js @@ -1,4 +1,7 @@ import * as Yup from 'yup'; +import { + getTargetPathFieldName +} from './AppFormUtils'; const FormSchema = (app) => { const appFields = { @@ -131,6 +134,29 @@ const FormSchema = (app) => { input.sourceUrl === null || typeof input.sourceUrl === 'undefined' ? '' : input.sourceUrl; + + // Add targetDir for all sourceUrl + const targetPathName = getTargetPathFieldName(input.name); + appFields.schema.fileInputs[targetPathName] = Yup.string(); + appFields.schema.fileInputs[targetPathName] = appFields.schema.fileInputs[ + targetPathName + ].matches( + /^tapis:\/\//g, + "Input file Target Directory must be a valid Tapis URI, starting with 'tapis://'" + ); + + appFields.schema.fileInputs[targetPathName] = false; + appFields.fileInputs[targetPathName] = { + label: 'Target Path for ' + input.name, + description: 'The target path is the location to which data are copied from the input. Empty target path or \'*\' indicates, the simple directory or file name from the input path is automatically assign to the target path.', + required: false, + readOnly: false, + type: 'text' + }; + appFields.defaults.fileInputs[targetPathName] = + input.targetPath === null || typeof input.targetPath === 'undefined' + ? '*' + : input.targetPath; }); return appFields; }; diff --git a/client/src/components/Applications/AppForm/AppFormUtils.js b/client/src/components/Applications/AppForm/AppFormUtils.js index 812fe3379..b0469348d 100644 --- a/client/src/components/Applications/AppForm/AppFormUtils.js +++ b/client/src/components/Applications/AppForm/AppFormUtils.js @@ -1,6 +1,8 @@ import * as Yup from 'yup'; import { getSystemName } from 'utils/systems'; +export const TARGET_PATH_FIELD_PREFIX = '_TargetPath_'; + export const getQueueMaxMinutes = (app, queueName) => { return app.exec_sys.batchLogicalQueues.find((q) => q.name === queueName) .maxMinutes; @@ -165,3 +167,36 @@ export const updateValuesForQueue = (app, values) => { return updatedValues; }; + +/** + * Get the field name used for target path in AppForm + * + * @function + * @param {String} inputFieldName + * @returns {String} field Name prefixed with target path + */ +export const getTargetPathFieldName = (inputFieldName) => { + return TARGET_PATH_FIELD_PREFIX + inputFieldName; +}; + +/** + * Whether a field name is a system defined field for Target Path + * + * @function + * @param {String} inputFieldName + * @returns {String} field Name suffixed with target path + */ +export const isTargetPathField = (inputFieldName) => { + return inputFieldName && inputFieldName.startsWith(TARGET_PATH_FIELD_PREFIX); +}; + +/** + * From target path field name, derive the original input field name. + * + * @function + * @param {String} targetPathFieldName + * @returns {String} actual field name + */ +export const getInputFieldFromTargetPathField = (targetPathFieldName) => { + return targetPathFieldName.replace(TARGET_PATH_FIELD_PREFIX,''); +};