diff --git a/CHANGELOG.md b/CHANGELOG.md
index a3fe09565..45d0180f1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [3.10.0]
+
+### Added
+
+- deps/react-18: Update React to v18 (#979)
+- WP-50: Fix sizing of buttons "as-link" (#986)
+- WP-509: Handle file/folder download feature with large number of files (#981)
+- WP-520: AppTray should use versionEnabled for list of apps instead of enabled (#991)
+- WP-24: Disabling Google Drive Integration (#988)
+- WP-730: Refactor useRename to use react-query (#993)
+- WP-728: Mutation hook: Copy file (#1000)
+- WP-78: V3 Shared Workspaces Tests (#987)
+
+### Fixed
+
+- WP-419 Public Data Header Left Margin (#1003)
+- WP-765: Fix job status button to show background (#1015)
+
+
## [3.9.0]
### Fixed
@@ -1115,7 +1134,8 @@ WP-306: Fix target path regression (#871)
## [1.0.0] - 2020-02-28
v1.0.0 Production release as of Feb 28, 2020.
-[unreleased]: https://github.com/TACC/Core-Portal/compare/v3.9.0...HEAD
+[unreleased]: https://github.com/TACC/Core-Portal/compare/v3.10.0...HEAD
+[3.10.0]: https://github.com/TACC/Core-Portal/releases/tag/v3.10.0
[3.9.0]: https://github.com/TACC/Core-Portal/releases/tag/v3.9.0
[3.8.2]: https://github.com/TACC/Core-Portal/releases/tag/v3.8.2
[3.8.1]: https://github.com/TACC/Core-Portal/releases/tag/v3.8.1
diff --git a/client/src/components/Jobs/JobsStatus/JobsStatus.jsx b/client/src/components/Jobs/JobsStatus/JobsStatus.jsx
index b3886feaa..c26fbcb3c 100644
--- a/client/src/components/Jobs/JobsStatus/JobsStatus.jsx
+++ b/client/src/components/Jobs/JobsStatus/JobsStatus.jsx
@@ -92,7 +92,7 @@ function JobsStatus({ status, fancy, jobUuid }) {
return (
{fancy && color ? (
-
+
{userStatus}
) : (
diff --git a/client/src/components/ManageAccount/tests/ManageAccountTables.test.jsx b/client/src/components/ManageAccount/tests/ManageAccountTables.test.jsx
index 548df3bbb..25194294f 100644
--- a/client/src/components/ManageAccount/tests/ManageAccountTables.test.jsx
+++ b/client/src/components/ManageAccount/tests/ManageAccountTables.test.jsx
@@ -156,6 +156,33 @@ describe('Third Party Apps', () => {
expect(getByText('Google Drive')).toBeDefined();
expect(getByText('Disconnect')).toBeDefined();
});
+ it('Shows potential 3rd party connections other than Google Drive', () => {
+ const testStore = mockStore({
+ profile: {
+ ...dummyState,
+ data: {
+ ...dummyState.data,
+ integrations: [
+ {
+ label: '3rd Party Service',
+ description: '3rd Party Service description',
+ activated: true,
+ },
+ ],
+ },
+ },
+ });
+ const { getByText, queryByText } = render(
+
+
+
+ );
+ expect(getByText(/3rd Party Apps/)).toBeInTheDocument();
+ // Check that Google Drive is not rendered
+ expect(queryByText('Google Drive')).toBeNull();
+ // Check that other integrations are rendered
+ expect(getByText('3rd Party Service')).toBeInTheDocument();
+ });
});
describe('License Cell', () => {
diff --git a/client/src/components/PublicData/PublicData.jsx b/client/src/components/PublicData/PublicData.jsx
index d32750e66..29afbee7e 100644
--- a/client/src/components/PublicData/PublicData.jsx
+++ b/client/src/components/PublicData/PublicData.jsx
@@ -119,6 +119,7 @@ const PublicDataListing = ({ canDownload, downloadCallback }) => {
disabled={!canDownload}
/>
}
+ contentLayoutName="oneColumn"
>
state.files.operationStatus.copy,
- shallowEqual
- );
-
- const setStatus = (newStatus) =>
- dispatch({
- type: 'DATA_FILES_SET_OPERATION_STATUS',
- payload: { operation: 'copy', status: newStatus },
- });
-
- const copy = ({ srcApi, destApi, destSystem, destPath, name, callback }) => {
- const filteredSelected = selected
- .filter((f) => status[f.id] !== 'SUCCESS')
- .map((f) => ({ ...f, api: srcApi }));
- dispatch({
- type: 'DATA_FILES_COPY',
- payload: {
- dest: { system: destSystem, path: destPath, api: destApi, name },
- src: filteredSelected,
- reloadCallback: callback,
- },
- });
- };
-
- return { copy, status, setStatus };
-}
-
-export default useCopy;
diff --git a/client/src/hooks/datafiles/mutations/useCopy.ts b/client/src/hooks/datafiles/mutations/useCopy.ts
new file mode 100644
index 000000000..3398feade
--- /dev/null
+++ b/client/src/hooks/datafiles/mutations/useCopy.ts
@@ -0,0 +1,157 @@
+import { useDispatch, useSelector, shallowEqual } from 'react-redux';
+import { useSelectedFiles } from 'hooks/datafiles';
+import Cookies from 'js-cookie';
+import { apiClient } from 'utils/apiClient';
+import { useMutation } from '@tanstack/react-query';
+import truncateMiddle from 'utils/truncateMiddle';
+
+export async function copyFileUtil({
+ api,
+ scheme,
+ system,
+ path,
+ filename,
+ filetype,
+ destApi,
+ destSystem,
+ destPath,
+ destPathName,
+}: {
+ api: string;
+ scheme: string;
+ system: string;
+ path: string;
+ filename: string;
+ filetype: string;
+ destApi: string;
+ destSystem: string;
+ destPath: string;
+ destPathName: string;
+}) {
+ let url: string, body: any;
+ if (api === destApi) {
+ url = `/api/datafiles/${api}/copy/${scheme}/${system}/${path}/`;
+ url = url.replace(/\/{2,}/g, '/');
+ body = {
+ dest_system: destSystem,
+ dest_path: destPath,
+ file_name: filename,
+ filetype,
+ dest_path_name: destPathName,
+ };
+ } else {
+ url = `/api/datafiles/transfer/${filetype}/`;
+ url = url.replace(/\/{2,}/g, '/');
+ body = {
+ src_api: api,
+ dest_api: destApi,
+ src_system: system,
+ dest_system: destSystem,
+ src_path: path,
+ dest_path: destPath,
+ dest_path_name: destPathName,
+ dirname: filename,
+ };
+ }
+
+ const response = await apiClient.put(url, body, {
+ headers: { 'X-CSRFToken': Cookies.get('csrftoken') || '' },
+ withCredentials: true,
+ });
+ return response.data;
+}
+
+function useCopy() {
+ const dispatch = useDispatch();
+
+ const { selectedFiles: selected } = useSelectedFiles();
+
+ const status = useSelector(
+ (state: any) => state.files.operationStatus.copy,
+ shallowEqual
+ );
+
+ const { scheme } = useSelector(
+ (state: any) => state.files.params.FilesListing
+ );
+ const setStatus = (newStatus: string) =>
+ dispatch({
+ type: 'DATA_FILES_SET_OPERATION_STATUS',
+ payload: { operation: 'copy', status: newStatus },
+ });
+
+ const { mutateAsync } = useMutation({ mutationFn: copyFileUtil });
+ const copy = ({
+ srcApi,
+ destApi,
+ destSystem,
+ destPath,
+ name,
+ callback,
+ }: {
+ srcApi: string;
+ destApi: string;
+ destSystem: string;
+ destPath: string;
+ name: string;
+ callback: any;
+ }) => {
+ const filteredSelected = selected
+ .filter((f: any) => status[f.id] !== 'SUCCESS')
+ .map((f: any) => ({ ...f, api: srcApi }));
+ const copyCalls: Promise[] = filteredSelected.map((file: any) => {
+ // Copy File
+ dispatch({
+ type: 'DATA_FILES_SET_OPERATION_STATUS_BY_KEY',
+ payload: { status: 'RUNNING', key: file.id, operation: 'copy' },
+ });
+ return mutateAsync(
+ {
+ api: file.api,
+ scheme: scheme,
+ system: file.system,
+ path: file.path,
+ filename: file.name,
+ filetype: file.type,
+ destApi,
+ destSystem,
+ destPath,
+ destPathName: name,
+ },
+ {
+ onSuccess: () => {
+ dispatch({
+ type: 'DATA_FILES_SET_OPERATION_STATUS_BY_KEY',
+ payload: { status: 'SUCCESS', key: file.id, operation: 'copy' },
+ });
+ },
+ onError: (error: any) => {
+ dispatch({
+ type: 'DATA_FILES_SET_OPERATION_STATUS_BY_KEY',
+ payload: { status: 'ERROR', key: file.id, operation: 'copy' },
+ });
+ },
+ }
+ );
+ });
+ // Result
+ Promise.all(copyCalls).then(() => {
+ dispatch({
+ type: 'DATA_FILES_TOGGLE_MODAL',
+ payload: { operation: 'copy', props: {} },
+ });
+ dispatch({
+ type: 'ADD_TOAST',
+ payload: {
+ message: `${
+ copyCalls.length > 1 ? `${copyCalls.length} files` : 'File'
+ } copied to ${truncateMiddle(`${destPath}`, 20) || '/'}`,
+ },
+ });
+ callback();
+ });
+ };
+ return { copy, status, setStatus };
+}
+
+export default useCopy;
diff --git a/server/portal/apps/workspace/api/views.py b/server/portal/apps/workspace/api/views.py
index 69571375f..af07ad54d 100644
--- a/server/portal/apps/workspace/api/views.py
+++ b/server/portal/apps/workspace/api/views.py
@@ -429,7 +429,8 @@ def get(self, request, job_uuid):
class AppsTrayView(BaseApiView):
def getPrivateApps(self, user):
tapis = user.tapis_oauth.client
- apps_listing = tapis.apps.getApps(select="version,id,notes", search="(enabled.eq.true)", listType="MINE")
+ # Only shows enabled versions of apps
+ apps_listing = tapis.apps.getApps(select="version,id,notes", search="(versionEnabled.eq.true)", listType="MINE")
my_apps = list(map(lambda app: {
"label": getattr(app.notes, 'label', app.id),
"version": app.version,
@@ -441,7 +442,8 @@ def getPrivateApps(self, user):
def getPublicApps(self, user):
tapis = user.tapis_oauth.client
- apps_listing = tapis.apps.getApps(select="version,id,notes", search="(enabled.eq.true)", listType="SHARED_PUBLIC")
+ # Only shows enabled versions of apps
+ apps_listing = tapis.apps.getApps(select="version,id,notes", search="(versionEnabled.eq.true)", listType="SHARED_PUBLIC")
categories = []
html_definitions = {}
# Traverse category records in descending priority
diff --git a/server/portal/settings/settings_default.py b/server/portal/settings/settings_default.py
index 5bb0592e8..421d62e19 100644
--- a/server/portal/settings/settings_default.py
+++ b/server/portal/settings/settings_default.py
@@ -110,14 +110,6 @@
'readOnly': False,
'hideSearchBar': False
},
- {
- 'name': 'Google Drive',
- 'system': 'googledrive',
- 'scheme': 'private',
- 'api': 'googledrive',
- 'icon': None,
- 'integration': 'portal.apps.googledrive_integration'
- }
]
########################