diff --git a/CHANGELOG-add-id-search-to-dialog.md b/CHANGELOG-add-id-search-to-dialog.md new file mode 100644 index 0000000000..c442f4f939 --- /dev/null +++ b/CHANGELOG-add-id-search-to-dialog.md @@ -0,0 +1 @@ +- Add the HuBMAP ID search bar component to the "Launch New Workspace" dialog on the "My Workspaces" landing page. \ No newline at end of file diff --git a/context/app/static/js/components/detailPage/ProcessedData/HelperPanel/HelperPanel.tsx b/context/app/static/js/components/detailPage/ProcessedData/HelperPanel/HelperPanel.tsx index 9d4e181a95..1dea866d0d 100644 --- a/context/app/static/js/components/detailPage/ProcessedData/HelperPanel/HelperPanel.tsx +++ b/context/app/static/js/components/detailPage/ProcessedData/HelperPanel/HelperPanel.tsx @@ -121,10 +121,14 @@ function WorkspaceButton() { const { control, errors, + removeDatasets, setDialogIsOpen: setOpenCreateWorkspace, dialogIsOpen: createWorkspaceIsOpen, ...rest - } = useCreateWorkspaceForm({ defaultName: currentDataset?.hubmap_id }); + } = useCreateWorkspaceForm({ + defaultName: currentDataset?.hubmap_id, + initialSelectedDatasets: currentDataset ? [currentDataset.uuid] : [], + }); const openEditWorkspaceDialog = useOpenDialog('ADD_DATASETS_FROM_SEARCH'); @@ -183,13 +187,7 @@ function WorkspaceButton() { Add to Workspace - + ); diff --git a/context/app/static/js/components/detailPage/entityHeader/EntityHeaderActionButtons/EntityHeaderActionButtons.tsx b/context/app/static/js/components/detailPage/entityHeader/EntityHeaderActionButtons/EntityHeaderActionButtons.tsx index 291fd1aa44..94a7b1cfb6 100644 --- a/context/app/static/js/components/detailPage/entityHeader/EntityHeaderActionButtons/EntityHeaderActionButtons.tsx +++ b/context/app/static/js/components/detailPage/entityHeader/EntityHeaderActionButtons/EntityHeaderActionButtons.tsx @@ -94,7 +94,10 @@ function CreateWorkspaceButton({ hubmap_id, mapped_data_access_level, }: Pick) { - const { setDialogIsOpen, ...rest } = useCreateWorkspaceForm({ defaultName: hubmap_id }); + const { setDialogIsOpen, removeDatasets, ...rest } = useCreateWorkspaceForm({ + defaultName: hubmap_id, + initialSelectedDatasets: [uuid], + }); const disabled = mapped_data_access_level === 'Protected'; @@ -108,7 +111,7 @@ function CreateWorkspaceButton({ tooltip={disabled ? 'Protected datasets are not available in Workspaces.' : 'Launch a new workspace.'} disabled={disabled} /> - + ); } diff --git a/context/app/static/js/components/detailPage/visualization/VisualizationNotebookButton/VisualizationNotebookButton.tsx b/context/app/static/js/components/detailPage/visualization/VisualizationNotebookButton/VisualizationNotebookButton.tsx index 72b0db9057..df4a1064af 100644 --- a/context/app/static/js/components/detailPage/visualization/VisualizationNotebookButton/VisualizationNotebookButton.tsx +++ b/context/app/static/js/components/detailPage/visualization/VisualizationNotebookButton/VisualizationNotebookButton.tsx @@ -24,9 +24,10 @@ function VisualizationNotebookButton({ uuid, hubmap_id, mapped_data_access_level const trackEntityPageEvent = useTrackEntityPageEvent(); const { toastError } = useSnackbarActions(); - const { setDialogIsOpen, ...rest } = useCreateWorkspaceForm({ + const { setDialogIsOpen, removeDatasets, ...rest } = useCreateWorkspaceForm({ defaultName: hubmap_id, defaultTemplate: 'visualization', + initialSelectedDatasets: [uuid], }); const downloadNotebook = useCallback(() => { @@ -57,7 +58,7 @@ function VisualizationNotebookButton({ uuid, hubmap_id, mapped_data_access_level return ( <> - + {options.map((props) => ( diff --git a/context/app/static/js/components/workspaces/AddDatasetsDialog/hooks.ts b/context/app/static/js/components/workspaces/AddDatasetsDialog/hooks.ts index 8f3b8535ff..9867a72c7d 100644 --- a/context/app/static/js/components/workspaces/AddDatasetsDialog/hooks.ts +++ b/context/app/static/js/components/workspaces/AddDatasetsDialog/hooks.ts @@ -4,7 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { Workspace } from '../types'; -import { useUpdateWorkspaceDatasets } from '../hooks'; +import { useUpdateWorkspaceDatasets, useWorkspaceDetail } from '../hooks'; import { datasetsField } from '../workspaceFormFields'; import { useDatasetsAutocomplete } from '../AddDatasetsTable'; import { useTooManyDatasetsErrors, useTooManyDatasetsWarnings } from '../formHooks'; @@ -51,6 +51,8 @@ function useAddDatasetsDialog({ workspace }: { workspace: Workspace }) { name: 'datasets', }); + const { workspaceDatasets: initialDatasets } = useWorkspaceDetail({ workspaceId }); + const { inputValue, setInputValue, @@ -62,7 +64,7 @@ function useAddDatasetsDialog({ workspace }: { workspace: Workspace }) { searchHits, resetAutocompleteState, } = useDatasetsAutocomplete({ - workspaceId, + workspaceDatasets: initialDatasets, selectedDatasets: field.value, updateDatasetsFormState: field.onChange, }); diff --git a/context/app/static/js/components/workspaces/AddDatasetsFromSearchDialog/hooks.ts b/context/app/static/js/components/workspaces/AddDatasetsFromSearchDialog/hooks.ts index 6f5af9d440..88f38fa4cc 100644 --- a/context/app/static/js/components/workspaces/AddDatasetsFromSearchDialog/hooks.ts +++ b/context/app/static/js/components/workspaces/AddDatasetsFromSearchDialog/hooks.ts @@ -4,7 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { useEditWorkspaceStore } from 'js/stores/useWorkspaceModalStore'; -import { useUpdateWorkspaceDatasets } from '../hooks'; +import { useUpdateWorkspaceDatasets, useWorkspaceDetail } from '../hooks'; import { datasetsField as datasetsFieldSchema, workspaceIdField as workspaceIdFieldSchema, @@ -102,6 +102,8 @@ function useAddDatasetsFromSearchDialog() { name: 'workspaceId', }); + const { workspaceDatasets: initialDatasets } = useWorkspaceDetail({ workspaceId: workspaceIdField.value }); + const { inputValue, setInputValue, @@ -113,7 +115,7 @@ function useAddDatasetsFromSearchDialog() { searchHits, resetAutocompleteState, } = useDatasetsAutocomplete({ - workspaceId: workspaceIdField.value, + workspaceDatasets: initialDatasets, selectedDatasets: datasetsField.value, updateDatasetsFormState: datasetsField.onChange, }); diff --git a/context/app/static/js/components/workspaces/AddDatasetsTable/AddDatasetsTable.tsx b/context/app/static/js/components/workspaces/AddDatasetsTable/AddDatasetsTable.tsx index 1c4a0a66a1..304cf29492 100644 --- a/context/app/static/js/components/workspaces/AddDatasetsTable/AddDatasetsTable.tsx +++ b/context/app/static/js/components/workspaces/AddDatasetsTable/AddDatasetsTable.tsx @@ -5,6 +5,7 @@ import Autocomplete, { AutocompleteRenderInputParams } from '@mui/material/Autoc import InputAdornment from '@mui/material/InputAdornment'; import Typography from '@mui/material/Typography'; +import { Alert } from 'js/shared-styles/alerts/Alert'; import { UseDatasetsAutocompleteReturnType, SearchAheadHit } from './hooks'; import WorkspaceDatasetsTable from '../WorkspaceDatasetsTable'; @@ -75,6 +76,7 @@ function AddDatasetsTable({ datasetsUUIDs={allDatasets} disabledIDs={new Set(workspaceDatasets)} removeDatasets={removeDatasets} + emptyAlert={No datasets available.} /> ); diff --git a/context/app/static/js/components/workspaces/AddDatasetsTable/hooks.ts b/context/app/static/js/components/workspaces/AddDatasetsTable/hooks.ts index d35b4e0755..6eec025046 100644 --- a/context/app/static/js/components/workspaces/AddDatasetsTable/hooks.ts +++ b/context/app/static/js/components/workspaces/AddDatasetsTable/hooks.ts @@ -4,8 +4,6 @@ import { useSearchHits } from 'js/hooks/useSearchData'; import { Dataset } from 'js/components/types'; import { useSnackbarActions } from 'js/shared-styles/snackbars/store'; -import { useWorkspaceDetail } from '../hooks'; - interface BuildIDPrefixQueryType { value: string; valuePrefix?: string; @@ -81,23 +79,26 @@ function useSearchAhead({ value, valuePrefix = '', uuidsToExclude = [] }: BuildI } function useDatasetsAutocomplete({ - workspaceId, + workspaceDatasets = [], selectedDatasets = [], updateDatasetsFormState, }: { - workspaceId: number; + workspaceDatasets?: string[]; selectedDatasets: string[]; updateDatasetsFormState: (datasetUUIDS: string[]) => void; }) { const [inputValue, setInputValue] = useState(''); + const [, setRefresh] = useState(false); const [autocompleteValue, setAutocompleteValue] = useState(null); const { toastSuccess } = useSnackbarActions(); const removeDatasets = useCallback( (uuids: string[]) => { - const selectedDatasetsSet = new Set(selectedDatasets); - uuids.forEach((uuid) => selectedDatasetsSet.delete(uuid)); - updateDatasetsFormState([...selectedDatasetsSet]); + const updatedDatasets = selectedDatasets.filter((uuid) => !uuids.includes(uuid)); + updateDatasetsFormState(updatedDatasets); + + // Trigger a re-render to update the table results + setRefresh((prev) => !prev); }, [selectedDatasets, updateDatasetsFormState], ); @@ -120,7 +121,6 @@ function useDatasetsAutocomplete({ [selectedDatasets, updateDatasetsFormState, resetAutocompleteState, toastSuccess], ); - const { workspaceDatasets } = useWorkspaceDetail({ workspaceId }); const allDatasets = [...workspaceDatasets, ...selectedDatasets]; const { searchHits } = useSearchAhead({ value: inputValue, valuePrefix: 'HBM', uuidsToExclude: allDatasets }); diff --git a/context/app/static/js/components/workspaces/NewWorkspaceDialog/NewWorkspaceDialog.tsx b/context/app/static/js/components/workspaces/NewWorkspaceDialog/NewWorkspaceDialog.tsx index 3741aab57a..95b52975ad 100644 --- a/context/app/static/js/components/workspaces/NewWorkspaceDialog/NewWorkspaceDialog.tsx +++ b/context/app/static/js/components/workspaces/NewWorkspaceDialog/NewWorkspaceDialog.tsx @@ -8,11 +8,14 @@ import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import LoadingButton from '@mui/lab/LoadingButton'; +import Typography from '@mui/material/Typography'; import Step, { StepDescription } from 'js/shared-styles/surfaces/Step'; +import { Alert } from 'js/shared-styles/alerts/Alert'; import WorkspaceField from 'js/components/workspaces/WorkspaceField'; import { useLaunchWorkspaceStore } from 'js/stores/useWorkspaceModalStore'; import { useSelectItems } from 'js/hooks/useSelectItems'; +import InternalLink from 'js/shared-styles/Links/InternalLink'; import { useWorkspaceTemplates } from './hooks'; import { CreateWorkspaceFormTypes } from './useCreateWorkspaceForm'; @@ -20,6 +23,8 @@ import { CreateTemplateNotebooksTypes } from '../types'; import WorkspaceDatasetsTable from '../WorkspaceDatasetsTable'; import TemplateSelectStep from '../TemplateSelectStep'; import WorkspaceJobTypeField from '../WorkspaceJobTypeField'; +import AddDatasetsTable from '../AddDatasetsTable'; +import { SearchAheadHit } from '../AddDatasetsTable/hooks'; const text = { overview: { @@ -31,10 +36,27 @@ const text = { }, datasets: { title: 'Edit Datasets Selection', - description: [ - 'To remove a dataset, select the dataset and press the delete button. If all datasets are removed, an empty workspace will be launched.', - 'To add more datasets to a workspace, you must navigate to the dataset search page, select datasets of interests and follow steps to launch a workspace from the search page. As a reminder, once you navigate away from this page, all selected datasets will be lost so take note of any HuBMAP IDs of interest, or copy IDs to your clipboard by selecting datasets in the table below and pressing the copy button. You can also save datasets to the “My Lists” feature.', - ], + description: { + searchBar: [ + + {' '} + Add datasets by HuBMAP ID below or navigate to the{' '} + dataset search page, select datasets and + follow steps to launch a workspace. + , + ], + all: [ + 'To remove a dataset, select the dataset and press the delete button. If all datasets are removed, an empty workspace will be launched.', + 'Once you navigate away from this page, all progress will be lost. You can copy IDs to your clipboard by selecting datasets in the table below and pressing the copy button. You can also save datasets to “My Lists”.', + , + ], + }, }, configure: { title: 'Configure Workspace', @@ -53,17 +75,23 @@ type ReactHookFormProps = Pick, 'handleS }; interface NewWorkspaceDialogProps { - datasetUUIDs?: Set; errorMessages?: string[]; dialogIsOpen: boolean; handleClose: () => void; - removeDatasets?: (datasetUUIDs: string[]) => void; + removeDatasets?: (uuids: string[]) => void; onSubmit: ({ workspaceName, templateKeys, uuids }: CreateTemplateNotebooksTypes) => void; isSubmitting?: boolean; + showDatasetsSearchBar?: boolean; + inputValue: string; + setInputValue: React.Dispatch>; + autocompleteValue: SearchAheadHit | null; + addDataset: (e: React.SyntheticEvent, newValue: SearchAheadHit | null) => void; + workspaceDatasets: string[]; + allDatasets: string[]; + searchHits: SearchAheadHit[]; } function NewWorkspaceDialog({ - datasetUUIDs = new Set(), errorMessages = [], dialogIsOpen, handleClose, @@ -71,9 +99,17 @@ function NewWorkspaceDialog({ control, errors, onSubmit, - removeDatasets, children, isSubmitting, + showDatasetsSearchBar, + inputValue, + setInputValue, + autocompleteValue, + addDataset, + removeDatasets, + workspaceDatasets, + allDatasets, + searchHits, }: PropsWithChildren) { const { selectedItems: selectedRecommendedTags, toggleItem: toggleTag } = useSelectItems([]); const [selectedTags, setSelectedTags] = useState([]); @@ -83,15 +119,20 @@ function NewWorkspaceDialog({ const { templates } = useWorkspaceTemplates([...selectedTags, ...selectedRecommendedTags]); const submit = useCallback( - ({ 'workspace-name': workspaceName, templates: templateKeys, workspaceJobTypeId }: CreateWorkspaceFormTypes) => { + ({ + 'workspace-name': workspaceName, + templates: templateKeys, + workspaceJobTypeId, + datasets, + }: CreateWorkspaceFormTypes) => { onSubmit({ workspaceName, templateKeys, - uuids: [...datasetUUIDs], + uuids: datasets, workspaceJobTypeId, }); }, - [datasetUUIDs, onSubmit], + [onSubmit], ); return ( @@ -114,9 +155,29 @@ function NewWorkspaceDialog({ {children} - - {datasetUUIDs.size > 0 && ( - + + {showDatasetsSearchBar && removeDatasets ? ( + + ) : ( + No datasets available.} + /> )} diff --git a/context/app/static/js/components/workspaces/NewWorkspaceDialog/NewWorkspaceDialogFromSelections.tsx b/context/app/static/js/components/workspaces/NewWorkspaceDialog/NewWorkspaceDialogFromSelections.tsx index 5f45b78c20..271e7a1e41 100644 --- a/context/app/static/js/components/workspaces/NewWorkspaceDialog/NewWorkspaceDialogFromSelections.tsx +++ b/context/app/static/js/components/workspaces/NewWorkspaceDialog/NewWorkspaceDialogFromSelections.tsx @@ -5,18 +5,26 @@ import SvgIcon from '@mui/material/SvgIcon'; import NewWorkspaceDialog from 'js/components/workspaces/NewWorkspaceDialog'; import ErrorOrWarningMessages from 'js/shared-styles/alerts/ErrorOrWarningMessages'; -import { useSelectableTableStore } from 'js/shared-styles/tables/SelectableTableProvider'; import WorkspacesIcon from 'assets/svg/workspaces.svg'; +import { useSelectableTableStore } from 'js/shared-styles/tables/SelectableTableProvider'; import { useCreateWorkspaceDatasets, useCreateWorkspaceForm } from './useCreateWorkspaceForm'; import RemoveProtectedDatasetsFormField from '../RemoveProtectedDatasetsFormField'; function NewWorkspaceDialogFromSelections() { - const { errorMessages, warningMessages, selectedRows, protectedHubmapIds, ...restWorkspaceDatasets } = - useCreateWorkspaceDatasets(); + const { + errorMessages, + warningMessages, + selectedRows, + protectedRows, + protectedHubmapIds, + removeProtectedDatasets, + ...restWorkspaceDatasets + } = useCreateWorkspaceDatasets(); const { deselectRows } = useSelectableTableStore(); - const { control, errors, setDialogIsOpen, ...rest } = useCreateWorkspaceForm({ + const { control, errors, setDialogIsOpen, removeDatasets, ...rest } = useCreateWorkspaceForm({ initialProtectedDatasets: protectedHubmapIds, + initialSelectedDatasets: [...selectedRows], }); return ( @@ -26,11 +34,13 @@ function NewWorkspaceDialogFromSelections() { Create New Workspace { + removeDatasets(uuids); + deselectRows(uuids); + }} {...rest} > @@ -38,6 +48,11 @@ function NewWorkspaceDialogFromSelections() { { + removeProtectedDatasets(); + removeDatasets(protectedRows.map((r) => r._id)); + }} + protectedRows={protectedRows} {...restWorkspaceDatasets} /> diff --git a/context/app/static/js/components/workspaces/NewWorkspaceDialog/NewWorkspaceDialogFromWorkspaceList.tsx b/context/app/static/js/components/workspaces/NewWorkspaceDialog/NewWorkspaceDialogFromWorkspaceList.tsx index 36fa24fcf1..c11e1500f2 100644 --- a/context/app/static/js/components/workspaces/NewWorkspaceDialog/NewWorkspaceDialogFromWorkspaceList.tsx +++ b/context/app/static/js/components/workspaces/NewWorkspaceDialog/NewWorkspaceDialogFromWorkspaceList.tsx @@ -6,12 +6,13 @@ import { useCreateWorkspaceForm } from './useCreateWorkspaceForm'; function NewWorkspaceDialogFromWorkspaceList() { const { setDialogIsOpen, ...rest } = useCreateWorkspaceForm({}); + return ( <> setDialogIsOpen(true)} tooltip="Create workspace"> - + ); } diff --git a/context/app/static/js/components/workspaces/NewWorkspaceDialog/hooks.ts b/context/app/static/js/components/workspaces/NewWorkspaceDialog/hooks.ts index 669c398b13..183e24c6f0 100644 --- a/context/app/static/js/components/workspaces/NewWorkspaceDialog/hooks.ts +++ b/context/app/static/js/components/workspaces/NewWorkspaceDialog/hooks.ts @@ -6,6 +6,7 @@ import { fetcher } from 'js/helpers/swr'; import { trackEvent } from 'js/helpers/trackers'; import { useSnackbarActions } from 'js/shared-styles/snackbars'; import { SWRError } from 'js/helpers/swr/errors'; + import { TemplatesResponse, CreateTemplateNotebooksTypes, TemplateTagsResponse, TemplatesTypes } from '../types'; import { useCreateAndLaunchWorkspace, useCreateTemplates } from '../hooks'; import { buildDatasetSymlinks } from '../utils'; diff --git a/context/app/static/js/components/workspaces/NewWorkspaceDialog/useCreateWorkspaceForm.ts b/context/app/static/js/components/workspaces/NewWorkspaceDialog/useCreateWorkspaceForm.ts index 3cdd958ad6..d0766376d2 100644 --- a/context/app/static/js/components/workspaces/NewWorkspaceDialog/useCreateWorkspaceForm.ts +++ b/context/app/static/js/components/workspaces/NewWorkspaceDialog/useCreateWorkspaceForm.ts @@ -10,9 +10,11 @@ import { protectedDatasetsField, templatesField, workspaceJobTypeIdField, + datasetsField, } from '../workspaceFormFields'; import { useProtectedDatasetsForm, useTooManyDatasetsErrors, useTooManyDatasetsWarnings } from '../formHooks'; import { DEFAULT_JOB_TYPE, DEFAULT_TEMPLATE_KEY } from '../constants'; +import { useDatasetsAutocomplete } from '../AddDatasetsTable/hooks'; export interface FormWithTemplates { templates: string[]; @@ -21,20 +23,33 @@ interface CreateWorkspaceFormTypes extends FormWithTemplates { 'workspace-name': string; 'protected-datasets': string; workspaceJobTypeId: string; + datasets: string[]; } interface UseCreateWorkspaceTypes { defaultName?: string; defaultTemplate?: string; initialProtectedDatasets?: string; + initialSelectedDatasets?: string[]; } const schema = z - .object({ ...workspaceNameField, ...protectedDatasetsField, ...templatesField, ...workspaceJobTypeIdField }) + .object({ + ...workspaceNameField, + ...protectedDatasetsField, + ...templatesField, + ...workspaceJobTypeIdField, + ...datasetsField, + }) .partial() .required({ 'workspace-name': true, templates: true }); -function useCreateWorkspaceForm({ defaultName, defaultTemplate, initialProtectedDatasets }: UseCreateWorkspaceTypes) { +function useCreateWorkspaceForm({ + defaultName, + defaultTemplate, + initialProtectedDatasets, + initialSelectedDatasets = [], +}: UseCreateWorkspaceTypes) { const [dialogIsOpen, setDialogIsOpen] = useState(false); const createTemplateNotebooks = useTemplateNotebooks(); @@ -45,6 +60,8 @@ function useCreateWorkspaceForm({ defaultName, defaultTemplate, initialProtected handleSubmit, control, reset, + getValues, + setValue, formState: { errors, isSubmitting, isSubmitSuccessful }, trigger, } = useForm({ @@ -53,11 +70,27 @@ function useCreateWorkspaceForm({ defaultName, defaultTemplate, initialProtected 'protected-datasets': checkedProtectedDatasets, templates: [defaultTemplate ?? DEFAULT_TEMPLATE_KEY], workspaceJobTypeId: DEFAULT_JOB_TYPE, + datasets: initialSelectedDatasets, }, mode: 'onChange', resolver: zodResolver(schema), }); + const { + inputValue, + setInputValue, + autocompleteValue, + addDataset, + removeDatasets, + workspaceDatasets, + allDatasets, + searchHits, + resetAutocompleteState, + } = useDatasetsAutocomplete({ + selectedDatasets: getValues('datasets'), + updateDatasetsFormState: (newDatasets) => setValue('datasets', newDatasets), + }); + function handleClose() { reset(); setDialogIsOpen(false); @@ -72,14 +105,9 @@ function useCreateWorkspaceForm({ defaultName, defaultTemplate, initialProtected useEffect(() => { if (initialProtectedDatasets && initialProtectedDatasets !== '') { - reset({ - 'workspace-name': checkedWorkspaceName, - 'protected-datasets': checkedProtectedDatasets, - templates: [defaultTemplate ?? DEFAULT_TEMPLATE_KEY], - workspaceJobTypeId: DEFAULT_JOB_TYPE, - }); + setValue('protected-datasets', initialProtectedDatasets); } - }, [initialProtectedDatasets, reset, checkedWorkspaceName, checkedProtectedDatasets, defaultTemplate]); + }, [initialProtectedDatasets, setValue]); useEffect(() => { if (dialogIsOpen) { @@ -98,6 +126,15 @@ function useCreateWorkspaceForm({ defaultName, defaultTemplate, initialProtected errors, onSubmit, isSubmitting: isSubmitting || isSubmitSuccessful, + inputValue, + setInputValue, + autocompleteValue, + addDataset, + removeDatasets, + workspaceDatasets, + allDatasets, + searchHits, + resetAutocompleteState, }; } diff --git a/context/app/static/js/components/workspaces/WorkspaceDatasetsTable/WorkspaceDatasetsTable.tsx b/context/app/static/js/components/workspaces/WorkspaceDatasetsTable/WorkspaceDatasetsTable.tsx index 4933450172..5dd85d53d2 100644 --- a/context/app/static/js/components/workspaces/WorkspaceDatasetsTable/WorkspaceDatasetsTable.tsx +++ b/context/app/static/js/components/workspaces/WorkspaceDatasetsTable/WorkspaceDatasetsTable.tsx @@ -29,8 +29,9 @@ interface WorkspaceDatasetsTableProps { addDatasets?: MergedWorkspace; label?: ReactNode; disabledIDs?: Set; - additionalAlerts?: ReactNode; + emptyAlert?: ReactNode; additionalButtons?: ReactNode; + hideTableIfEmpty?: boolean; } function WorkspaceDatasetsTable({ @@ -39,8 +40,9 @@ function WorkspaceDatasetsTable({ addDatasets, label, disabledIDs, - additionalAlerts, + emptyAlert, additionalButtons, + hideTableIfEmpty, }: WorkspaceDatasetsTableProps) { const { selectedRows } = useSelectableTableStore(); const query = useMemo( @@ -64,6 +66,7 @@ function WorkspaceDatasetsTable({ const datasetsPresent = datasetsUUIDs.length > 0; const hasMaxDatasets = addDatasets && isWorkspaceAtDatasetLimit(addDatasets); + const hideTable = hideTableIfEmpty && !datasetsPresent; return ( @@ -93,13 +96,14 @@ function WorkspaceDatasetsTable({ } /> - {datasetsPresent ? ( + {hideTable ? ( + emptyAlert + ) : ( entities={[{ query, columns, entityType: 'Dataset' }]} disabledIDs={disabledIDs} + emptyAlert={emptyAlert} /> - ) : ( - additionalAlerts )} ); diff --git a/context/app/static/js/components/workspaces/workspaceFormFields.ts b/context/app/static/js/components/workspaces/workspaceFormFields.ts index 612a9e9076..8662d38281 100644 --- a/context/app/static/js/components/workspaces/workspaceFormFields.ts +++ b/context/app/static/js/components/workspaces/workspaceFormFields.ts @@ -27,11 +27,9 @@ const templatesField = { }; const datasetsField = { - datasets: z - .array(z.string(), { - errorMap: withCustomMessage('At least one dataset must be selected. Please select a dataset.'), - }) - .nonempty(), + datasets: z.array(z.string(), { + errorMap: withCustomMessage('At least one dataset must be selected. Please select a dataset.'), + }), }; const workspaceIdField = { diff --git a/context/app/static/js/pages/Workspace/Workspace.tsx b/context/app/static/js/pages/Workspace/Workspace.tsx index 707a8a5dfe..b86d976694 100644 --- a/context/app/static/js/pages/Workspace/Workspace.tsx +++ b/context/app/static/js/pages/Workspace/Workspace.tsx @@ -98,7 +98,8 @@ function WorkspaceContent({ workspaceId }: WorkspacePageProps) { datasetsUUIDs={workspaceDatasets} addDatasets={workspace} label={Datasets} - additionalAlerts={ + hideTableIfEmpty + emptyAlert={ {blocks.map((block) => ( - {block} + {block} ))} ); diff --git a/context/app/static/js/shared-styles/tables/EntitiesTable/EntitiesTables.tsx b/context/app/static/js/shared-styles/tables/EntitiesTable/EntitiesTables.tsx index 93eb9280c7..7800291f20 100644 --- a/context/app/static/js/shared-styles/tables/EntitiesTable/EntitiesTables.tsx +++ b/context/app/static/js/shared-styles/tables/EntitiesTable/EntitiesTables.tsx @@ -3,15 +3,18 @@ import React from 'react'; import { useTabs } from 'js/shared-styles/tabs'; import { useSearchTotalHitsCounts } from 'js/hooks/useSearchData'; import { entityIconMap } from 'js/shared-styles/icons/entityIconMap'; + import EntityTable from './EntityTable'; import { EntitiesTabTypes } from './types'; import { Tabs, Tab, TabPanel } from '../TableTabs'; +import { StyledPaper } from './style'; interface EntitiesTablesProps { isSelectable?: boolean; initialTabIndex?: number; entities: EntitiesTabTypes[]; disabledIDs?: Set; + emptyAlert?: React.ReactNode; } function EntitiesTables({ @@ -19,6 +22,7 @@ function EntitiesTables({ initialTabIndex = 0, entities, disabledIDs, + emptyAlert, }: EntitiesTablesProps) { const { openTabIndex, handleTabChange } = useTabs(initialTabIndex); @@ -27,6 +31,8 @@ function EntitiesTables({ isLoading: boolean; }; + const tableIsEmpty = entities[0].query.query?.ids?.values?.length === 0; + return ( <> @@ -44,11 +50,15 @@ function EntitiesTables({ ); })} - {entities.map(({ query, columns, entityType }, i) => ( - - query={query} columns={columns} isSelectable={isSelectable} disabledIDs={disabledIDs} /> - - ))} + {tableIsEmpty ? ( + {emptyAlert} + ) : ( + entities.map(({ query, columns, entityType }, i) => ( + + query={query} columns={columns} isSelectable={isSelectable} disabledIDs={disabledIDs} /> + + )) + )} ); } diff --git a/context/app/static/js/shared-styles/tables/EntitiesTable/style.ts b/context/app/static/js/shared-styles/tables/EntitiesTable/style.ts index 780d9b7b49..113cdd3634 100644 --- a/context/app/static/js/shared-styles/tables/EntitiesTable/style.ts +++ b/context/app/static/js/shared-styles/tables/EntitiesTable/style.ts @@ -1,3 +1,4 @@ +import Paper from '@mui/material/Paper'; import { styled } from '@mui/material/styles'; const StyledDiv = styled('div')({ @@ -8,4 +9,8 @@ const StyledDiv = styled('div')({ maxHeight: '340px', // Cuts off the last row partially to cue users to scroll. }); -export { StyledDiv }; +const StyledPaper = styled(Paper)(({ theme }) => ({ + padding: theme.spacing(1), +})); + +export { StyledDiv, StyledPaper };