diff --git a/client/src/components/Applications/AppForm/AppForm.jsx b/client/src/components/Applications/AppForm/AppForm.jsx
index 4cb3cfbf9..d1986e250 100644
--- a/client/src/components/Applications/AppForm/AppForm.jsx
+++ b/client/src/components/Applications/AppForm/AppForm.jsx
@@ -19,10 +19,14 @@ import { Link } from 'react-router-dom';
import { getSystemName } from 'utils/systems';
import FormSchema from './AppFormSchema';
import {
+ checkAndSetDefaultTargetPath,
+ isTargetPathField,
+ getInputFieldFromTargetPathField,
getQueueMaxMinutes,
getMaxMinutesValidation,
getNodeCountValidation,
getCoresPerNodeValidation,
+ getTargetPathFieldName,
updateValuesForQueue,
} from './AppFormUtils';
import DataFilesSelectModal from '../../DataFiles/DataFilesModals/DataFilesSelectModal';
@@ -459,17 +463,37 @@ export const AppSchemaForm = ({ app }) => {
onSubmit={(values, { setSubmitting, resetForm }) => {
const job = cloneDeep(values);
- job.fileInputs = Object.entries(job.fileInputs)
- .map(([k, v]) => {
- // filter out read only inputs. 'FIXED' inputs are tracked as readOnly
- if (
- Object.hasOwn(appFields.fileInputs, k) &&
- appFields.fileInputs[k].readOnly
- )
- return;
- return { name: k, sourceUrl: v };
- })
- .filter((fileInput) => fileInput && fileInput.sourceUrl); // filter out any empty values
+ // Transform input field values into format that jobs service wants
+ job.fileInputs = Object.values(
+ Object.entries(job.fileInputs)
+ .map(([k, v]) => {
+ // filter out read only inputs. 'FIXED' inputs are tracked as readOnly
+ if (
+ Object.hasOwn(appFields.fileInputs, k) &&
+ appFields.fileInputs[k].readOnly
+ )
+ return;
+ 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(
{},
@@ -559,8 +583,15 @@ export const AppSchemaForm = ({ app }) => {
{Object.entries(appFields.fileInputs).map(
([name, field]) => {
- // TODOv3 handle fileInputArrays https://jira.tacc.utexas.edu/browse/TV3-81
- return (
+ // TODOv3 handle fileInputArrays https://jira.tacc.utexas.edu/browse/WP-81
+ return isTargetPathField(name) ? (
+
+ ) : (
{
const appFields = {
@@ -131,6 +135,28 @@ 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: field.readOnly,
+ type: 'text',
+ };
+ appFields.defaults.fileInputs[targetPathName] =
+ checkAndSetDefaultTargetPath(input.targetPath);
});
return appFields;
};
diff --git a/client/src/components/Applications/AppForm/AppFormUtils.js b/client/src/components/Applications/AppForm/AppFormUtils.js
index 812fe3379..1b5b30c7e 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,57 @@ 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, '');
+};
+
+/**
+ * Sets the default value if target path is not set.
+ *
+ * @function
+ * @param {String} targetPathFieldValue
+ * @returns {String} target path value
+ */
+export const checkAndSetDefaultTargetPath = (targetPathFieldValue) => {
+ if (targetPathFieldValue === null || targetPathFieldValue === undefined) {
+ return '*';
+ }
+
+ targetPathFieldValue = targetPathFieldValue.trim();
+
+ if (targetPathFieldValue.trim() === '') {
+ return '*';
+ }
+
+ return targetPathFieldValue;
+};