diff --git a/jsapp/js/projects/customViewRoute.tsx b/jsapp/js/projects/customViewRoute.tsx
index a2aa37e6e7..2759f6a70d 100644
--- a/jsapp/js/projects/customViewRoute.tsx
+++ b/jsapp/js/projects/customViewRoute.tsx
@@ -21,7 +21,9 @@ import styles from './projectViews.module.scss';
import {toJS} from 'mobx';
import {ROOT_URL} from 'js/constants';
import {fetchPostUrl} from 'js/api';
+import ProjectQuickActionsEmpty from './projectsTable/projectQuickActionsEmpty';
import ProjectQuickActions from './projectsTable/projectQuickActions';
+import ProjectBulkActions from './projectsTable/projectBulkActions';
function CustomViewRoute() {
const {viewUid} = useParams();
@@ -100,18 +102,32 @@ function CustomViewRoute() {
onClick={exportAllData}
/>
+ {selectedAssets.length === 0 && (
+
+
)}
+
+ {selectedAssets.length > 1 && (
+
+ )}
+ {selectedAssets.length === 0 && (
+
+ )}
+
{selectedAssets.length === 1 && (
diff --git a/jsapp/js/projects/projectViews.module.scss b/jsapp/js/projects/projectViews.module.scss
index 70aa0a175b..6945b4a817 100644
--- a/jsapp/js/projects/projectViews.module.scss
+++ b/jsapp/js/projects/projectViews.module.scss
@@ -9,8 +9,9 @@
.header {
@include mixins.centerRowFlex;
- gap: sizes.$x30;
padding: sizes.$x30 sizes.$x30 sizes.$x40;
+ gap: sizes.$x10 sizes.$x30;
+ flex-wrap: wrap;
}
.actions {
diff --git a/jsapp/js/projects/projectsTable/projectActions.module.scss b/jsapp/js/projects/projectsTable/projectActions.module.scss
index 8c65c8f8cf..50e8a32090 100644
--- a/jsapp/js/projects/projectsTable/projectActions.module.scss
+++ b/jsapp/js/projects/projectsTable/projectActions.module.scss
@@ -6,15 +6,3 @@
@include mixins.centerRowFlex;
gap: sizes.$x10;
}
-
-.menu {
- @include mixins.floatingRoundedBox;
- padding: sizes.$x6;
- min-width: sizes.$x180;
-
- // There is a `isFullWidth` property on Button component, but it also has text
- // centering styles on it, so we can't use it.
- :global .k-button {
- width: 100%;
- }
-}
diff --git a/jsapp/js/projects/projectsTable/projectBulkActions.tsx b/jsapp/js/projects/projectsTable/projectBulkActions.tsx
index d202f070fe..192b82d52f 100644
--- a/jsapp/js/projects/projectsTable/projectBulkActions.tsx
+++ b/jsapp/js/projects/projectsTable/projectBulkActions.tsx
@@ -3,29 +3,68 @@ import type {AssetResponse, ProjectViewAsset} from 'js/dataInterface';
import Button from 'js/components/common/button';
import actionsStyles from './projectActions.module.scss';
import BulkDeletePrompt from './bulkActions/bulkDeletePrompt';
+import {userCan} from 'js/components/permissions/utils';
interface ProjectBulkActionsProps {
/** A list of selected assets for bulk operations. */
assets: Array
;
}
+function userCanDeleteAssets(assets: Array) {
+ return assets.every((asset) => userCan('manage_asset', asset));
+}
+
+/**
+ * "Bulk" Quick Actions buttons. Use these when two or more projects are
+ * selected in the Project Table.
+ */
export default function ProjectBulkActions(props: ProjectBulkActionsProps) {
const [isDeletePromptOpen, setIsDeletePromptOpen] = useState(false);
+ const canBulkDelete = userCanDeleteAssets(props.assets);
+
+ let tooltipForDelete = t('Delete projects');
+ if (canBulkDelete) {
+ tooltipForDelete = t('Delete ##count## projects').replace(
+ '##count##',
+ String(props.assets.length)
+ );
+ }
return (
-
setIsDeletePromptOpen(true)}
- classNames={['right-tooltip']}
- />
+ {/* Archive / Unarchive - Bulk action not supported yet */}
+
+
+
+
+ {/* Share - Bulk action not supported yet */}
+
+
+
+
+ {/* Delete */}
+
+ setIsDeletePromptOpen(true)}
+ />
+
{isDeletePromptOpen && (
- openInFormBuilder(props.asset.uid)}
- />
+ {/* Archive / Unarchive */}
+ {/* Archive a deployed project */}
+ {props.asset.deployment_status === 'deployed' && (
+
+
+ archiveAsset(props.asset, (response: DeploymentResponse) => {
+ customViewStore.handleAssetChanged(response.asset);
+ })
+ }
+ />
+
+ )}
+ {/* Un-archive a deployed project */}
+ {props.asset.deployment_status === 'archived' && (
+
+
+ unarchiveAsset(props.asset, (response: DeploymentResponse) => {
+ customViewStore.handleAssetChanged(response.asset);
+ })
+ }
+ />
+
+ )}
+ {/* Show tooltip, since drafts can't be archived/unarchived */}
+ {props.asset.deployment_status === 'draft' && (
+
+
+
+ )}
- {props.asset.deployment__active && (
+ {/* Share */}
+
- archiveAsset(props.asset, (response: DeploymentResponse) => {
- customViewStore.handleAssetChanged(response.asset);
- })
- }
+ startIcon='user-share'
+ onClick={() => manageAssetSharing(props.asset.uid)}
/>
- )}
+
- {!props.asset.deployment__active && (
+ {/* Delete */}
+
- unarchiveAsset(props.asset, (response: DeploymentResponse) => {
- customViewStore.handleAssetChanged(response.asset);
- })
+ deleteAsset(
+ props.asset,
+ getAssetDisplayName(props.asset).final,
+ (deletedAssetUid: string) => {
+ customViewStore.handleAssetsDeleted([deletedAssetUid]);
+ }
+ )
}
/>
- )}
-
- manageAssetSharing(props.asset.uid)}
- />
-
-
- deleteAsset(
- props.asset,
- getAssetDisplayName(props.asset).final,
- (deletedAssetUid: string) => {
- customViewStore.handleAssetsDeleted([deletedAssetUid]);
- }
- )
- }
- />
-
-
- }
- menuContent={
-
- cloneAsset(props.asset)}
- label={t('Clone')}
- />
-
-
- deployAsset(props.asset, (response: DeploymentResponse) => {
- customViewStore.handleAssetChanged(response.asset);
- })
- }
- label={t('Deploy')}
- />
-
- replaceAssetForm(props.asset)}
- label={t('Replace form')}
- />
-
- manageAssetLanguages(props.asset.uid)}
- label={t('Manage translations')}
- />
-
- {'downloads' in props.asset &&
- props.asset.downloads.map((file) => {
- let icon: IconName = 'file';
- if (file.format === 'xml') {
- icon = 'file-xml';
- } else if (file.format === 'xls') {
- icon = 'file-xls';
- }
-
- return (
- downloadUrl(file.url)}
- label={
-
- {t('Download')}
- {file.format.toString().toUpperCase()}
-
- }
- />
- );
- })}
-
- {props.asset.asset_type === ASSET_TYPES.survey.id && (
-
- )}
-
- {props.asset.asset_type === ASSET_TYPES.template.id && (
-
- )}
-
- }
- />
+
);
}
diff --git a/jsapp/js/projects/projectsTable/projectQuickActionsEmpty.tsx b/jsapp/js/projects/projectsTable/projectQuickActionsEmpty.tsx
new file mode 100644
index 0000000000..224f80bb6d
--- /dev/null
+++ b/jsapp/js/projects/projectsTable/projectQuickActionsEmpty.tsx
@@ -0,0 +1,57 @@
+import React from 'react';
+import Button from 'js/components/common/button';
+import styles from './projectActions.module.scss';
+
+const NO_PROJECT_SELECTED = t('No project selected');
+
+/**
+ * Inactive Quick Actions buttons. Show these when zero projects are selected
+ * in the Project Table.
+ */
+export default function ProjectQuickActionsEmpty() {
+ return (
+
+ {/* Archive / Unarchive */}
+
+
+
+
+ {/* Share */}
+
+
+
+
+ {/* Delete */}
+
+
+
+
+ );
+}
diff --git a/jsapp/js/projects/projectsTable/projectsTable.tsx b/jsapp/js/projects/projectsTable/projectsTable.tsx
index 6aeed14dd4..4f309676f5 100644
--- a/jsapp/js/projects/projectsTable/projectsTable.tsx
+++ b/jsapp/js/projects/projectsTable/projectsTable.tsx
@@ -44,7 +44,7 @@ interface ProjectsTableProps {
}
/**
- * Displays a table of assets.
+ * Displays a table of assets. Works with `survey` type.
*/
export default function ProjectsTable(props: ProjectsTableProps) {
// We ensure name is always visible: