{
const appFields = {
@@ -97,6 +101,9 @@ const FormSchema = (app) => {
}
);
+ // The default is to not show target path for file inputs.
+ const showTargetPathForFileInputs =
+ app.definition.notes.showTargetPath ?? false;
(app.definition.jobAttributes.fileInputs || []).forEach((i) => {
const input = i;
/* TODOv3 consider hidden file inputs https://jira.tacc.utexas.edu/browse/WP-102
@@ -131,6 +138,33 @@ const FormSchema = (app) => {
input.sourceUrl === null || typeof input.sourceUrl === 'undefined'
? ''
: input.sourceUrl;
+
+ // Add targetDir for all sourceUrl
+ if (!showTargetPathForFileInputs) {
+ return;
+ }
+ 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 name of the ' +
+ input.name +
+ ' after it is copied to the target system, but before the job is run. Leave this value blank to just use the name of the input file.',
+ 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;
+};
diff --git a/client/src/components/DataFiles/DataFilesModals/DataFilesMoveModal.jsx b/client/src/components/DataFiles/DataFilesModals/DataFilesMoveModal.jsx
index 0f079795a..1faa2995e 100644
--- a/client/src/components/DataFiles/DataFilesModals/DataFilesMoveModal.jsx
+++ b/client/src/components/DataFiles/DataFilesModals/DataFilesMoveModal.jsx
@@ -50,10 +50,7 @@ const DataFilesMoveModal = React.memo(() => {
const onOpened = () => {
fetchListing({
- api: 'tapis',
- scheme: 'private',
- system: selectedSystem.system,
- path: `${selectedSystem.homeDir || ''}`,
+ ...params,
});
};
@@ -68,7 +65,7 @@ const DataFilesMoveModal = React.memo(() => {
setDisabled(true);
move({
destSystem: system,
- destPath: path,
+ destPath: path || '/',
callback: reloadPage,
});
},
diff --git a/client/src/components/Jobs/Jobs.jsx b/client/src/components/Jobs/Jobs.jsx
index bbef04a96..1945d1bf7 100644
--- a/client/src/components/Jobs/Jobs.jsx
+++ b/client/src/components/Jobs/Jobs.jsx
@@ -75,11 +75,14 @@ function JobsView({
const infiniteScrollCallback = useCallback(() => {
// TODOv3: dropV2Jobs
const dispatchType = version === 'v3' ? 'GET_JOBS' : 'GET_V2_JOBS';
- dispatch({
- type: dispatchType,
- params: { offset: jobs.length, queryString: query.query_string || '' },
- });
- }, [dispatch, jobs, query.query_string]);
+
+ if (!isJobLoading) {
+ dispatch({
+ type: dispatchType,
+ params: { offset: jobs.length, queryString: query.query_string || '' },
+ });
+ }
+ }, [dispatch, jobs, query.query_string, isJobLoading]);
const jobDetailLink = useCallback(
({
@@ -217,22 +220,20 @@ function JobsView({
disabled={isJobLoading || isNotificationLoading}
/>
)}
-
-
- {noDataText}
-
- }
- getRowProps={rowProps}
- columnMemoProps={[version]} /* TODOv3: dropV2Jobs. */
- />
-
+
+ {noDataText}
+
+ }
+ getRowProps={rowProps}
+ columnMemoProps={[version]} /* TODOv3: dropV2Jobs. */
+ />
>
);
}
diff --git a/client/src/components/Jobs/Jobs.test.js b/client/src/components/Jobs/Jobs.test.js
index ea30d1934..05dcb53f6 100644
--- a/client/src/components/Jobs/Jobs.test.js
+++ b/client/src/components/Jobs/Jobs.test.js
@@ -1,5 +1,5 @@
import React from 'react';
-import { render } from '@testing-library/react';
+import { fireEvent, render, waitFor } from '@testing-library/react';
import Jobs from './Jobs';
import { createMemoryHistory } from 'history';
import { default as jobsList } from './Jobs.fixture';
@@ -135,4 +135,35 @@ describe('Jobs View', () => {
const { getByText } = renderJobsComponent(store, history);
expect(getByText(/unable to retrieve your jobs/)).toBeDefined();
});
+
+ it('should dispatch another get jobs event on scroll with proper offset', async () => {
+ const store = mockStore({
+ notifications,
+ jobs: { ...jobs, list: jobsList },
+ workbench: { ...workbench, config: { hideDataFiles: false } },
+ apps: {
+ appIcons: {},
+ },
+ });
+
+ const { container } = render(
+
+
+
+
+
+ );
+
+ const scrollContainer = container.querySelector('.table-container');
+
+ fireEvent.scroll(scrollContainer, { target: { scrollTop: 1 } });
+
+ expect(store.getActions()).toEqual([
+ { type: 'GET_JOBS', params: { offset: 0, queryString: '' } },
+ {
+ type: 'GET_JOBS',
+ params: { offset: jobsList.length, queryString: '' },
+ },
+ ]);
+ });
});
diff --git a/client/src/components/_common/InfiniteScrollTable/InfiniteScrollTable.jsx b/client/src/components/_common/InfiniteScrollTable/InfiniteScrollTable.jsx
index f8496eeb8..a3bbe85e5 100644
--- a/client/src/components/_common/InfiniteScrollTable/InfiniteScrollTable.jsx
+++ b/client/src/components/_common/InfiniteScrollTable/InfiniteScrollTable.jsx
@@ -62,51 +62,60 @@ const InfiniteScrollTable = ({
useTable({ columns, data });
const onScroll = ({ target }) => {
- const bottom =
- target.scrollHeight - target.scrollTop === target.clientHeight;
+ const scrollbarHeight = target.offsetHeight - target.clientHeight;
+ const clientRectHeight = target.getBoundingClientRect().height;
+ const clientCalcHeight = clientRectHeight - scrollbarHeight;
+ const difference = Math.floor(target.scrollHeight - target.scrollTop);
+
+ const bottom = difference <= clientCalcHeight;
+
if (bottom && target.scrollTop > 0) {
onInfiniteScroll(tableData.length);
}
};
return (
-
-
- {headerGroups.map((headerGroup) => (
-
- {headerGroup.headers.map((column) => (
- {column.render('Header')} |
- ))}
-
- ))}
-
-
- {rows.map((row) => {
- prepareRow(row);
- return (
-
- {row.cells.map((cell) => {
- return (
-
- {cell.render('Cell')}
- |
- );
- })}
+
+
+
+ {headerGroups.map((headerGroup) => (
+
+ {headerGroup.headers.map((column) => (
+ {column.render('Header')} |
+ ))}
- );
- })}
-
-
-
-
+ ))}
+
+
+ {rows.map((row) => {
+ prepareRow(row);
+ return (
+
+ {row.cells.map((cell) => {
+ return (
+
+ {cell.render('Cell')}
+ |
+ );
+ })}
+
+ );
+ })}
+
+
+
+
+
);
};
diff --git a/client/src/components/_common/InfiniteScrollTable/InfiniteScrollTable.scss b/client/src/components/_common/InfiniteScrollTable/InfiniteScrollTable.scss
index 55b98d918..4361f7586 100644
--- a/client/src/components/_common/InfiniteScrollTable/InfiniteScrollTable.scss
+++ b/client/src/components/_common/InfiniteScrollTable/InfiniteScrollTable.scss
@@ -79,3 +79,8 @@
width: 100%;
}
}
+
+.table-container {
+ height: 100%;
+ overflow: scroll;
+}
diff --git a/client/src/redux/sagas/datafiles.sagas.js b/client/src/redux/sagas/datafiles.sagas.js
index 730a63c44..2ef66a565 100644
--- a/client/src/redux/sagas/datafiles.sagas.js
+++ b/client/src/redux/sagas/datafiles.sagas.js
@@ -254,7 +254,7 @@ export function* renameFile(action) {
action.payload.api,
action.payload.scheme,
file.system,
- file.path,
+ '/' + file.path,
action.payload.newName
);
yield put({
diff --git a/server/portal/apps/workspace/api/views.py b/server/portal/apps/workspace/api/views.py
index 9a94db829..f76b36402 100644
--- a/server/portal/apps/workspace/api/views.py
+++ b/server/portal/apps/workspace/api/views.py
@@ -179,7 +179,7 @@ def listing(self, client, request):
data = client.jobs.getJobSearchList(
limit=limit,
- startAfter=offset,
+ skip=offset,
orderBy='lastUpdated(desc),name(asc)',
_tapis_query_parameters={'tags.contains': f'portalName: {portal_name}'},
select='allAttributes'