Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement folder specific search for datasets #6677

Merged
merged 32 commits into from
Dec 8, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1ea19bc
fix invalid active folder when switching to another organization
philippotto Nov 30, 2022
3db78db
adapt wording in sidebar when SEARCH_RESULTS_LIMIT is exceeded
philippotto Nov 30, 2022
c0d0e6e
dont create folder when user hit escape in new-name-prompt
philippotto Dec 1, 2022
8a3e2f4
rename 'default' to Administrators & Dataset Managers
philippotto Dec 1, 2022
fe54b9d
show drag preview image when dragging a dataset row
philippotto Dec 1, 2022
94848bc
add file icons
philippotto Dec 1, 2022
16d46ca
extend comment
philippotto Dec 1, 2022
76eb582
don't select a folder when an entry of its context menu was selected
philippotto Dec 1, 2022
7276b10
select folder root when deleting active folder
philippotto Dec 1, 2022
0a606bd
update changelog
philippotto Dec 1, 2022
cd9711c
fix default value when setting recommended settings for task type
philippotto Dec 1, 2022
10d195b
implement folder-specific search
philippotto Dec 2, 2022
17e09a8
implement recursive folder search in backend and adapt frontend accor…
philippotto Dec 2, 2022
7285fd7
format
philippotto Dec 2, 2022
e950557
only show breadcrumbs OR datastore in dataset table
philippotto Dec 2, 2022
43a6d9b
improve UI for recursive configuration of search
philippotto Dec 5, 2022
d014011
Merge branch 'master' of github.com:scalableminds/webknossos into fol…
philippotto Dec 5, 2022
3466d40
update changelog
philippotto Dec 5, 2022
cee5f02
add folder icon to breadcrumb tag
philippotto Dec 5, 2022
701a2e9
invalidate search results when mutating a dataset
philippotto Dec 5, 2022
f946290
change search logic to implement three cases (global, folder, recursi…
philippotto Dec 6, 2022
bef4bc7
get rid of folderIdForSearch by unifying it with activeFolderId
philippotto Dec 7, 2022
f863eba
remember most recently used folder so that that folder can be re-acti…
philippotto Dec 7, 2022
2cc4318
clean up
philippotto Dec 7, 2022
edcb920
use fancy-schmancy match expression
philippotto Dec 7, 2022
cbb6ea0
Merge branch 'master' into folder-specific-search
philippotto Dec 7, 2022
e89b7a0
rename recursive to includeSubfolders
philippotto Dec 7, 2022
ab26e26
rename falsy to nullable in usePrevious
philippotto Dec 7, 2022
3b7db93
fix indexing of empty array and rename ignoreNullableValue to ignoreN…
philippotto Dec 7, 2022
3f29d02
fix get new task button in task tab in dashboard
philippotto Dec 8, 2022
2ae8674
update changelog
philippotto Dec 8, 2022
671abc9
Merge branch 'master' into folder-specific-search
philippotto Dec 8, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -751,7 +751,7 @@ function BreadcrumbsTag({ parts: allParts }: { parts: string[] | null }) {
}

return (
<Tooltip title={`This folder is located in ${formatPath(allParts)}.`}>
<Tooltip title={`This dataset is located in ${formatPath(allParts)}.`}>
<Tag>
<FolderOpenOutlined />
{formatPath(parts)}
Expand Down
104 changes: 51 additions & 53 deletions frontend/javascripts/dashboard/dataset/dataset_collection_context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
useFolderQuery,
} from "./queries";
import { useIsMutating } from "@tanstack/react-query";
import { usePrevious } from "libs/react_hooks";

type Options = {
datasetFilteringMode?: DatasetFilteringMode;
Expand All @@ -34,12 +35,11 @@ export type DatasetCollectionContextValue = {
) => Promise<void>;
updateCachedDataset: (dataset: APIDataset) => Promise<void>;
activeFolderId: string | null;
setActiveFolderId: (id: string) => void;
setActiveFolderId: (id: string | null) => void;
mostRecentlyUsedActiveFolderId: string | null;
supportsFolders: true;
globalSearchQuery: string | null;
setGlobalSearchQuery: (val: string | null) => void;
folderIdForSearch: string | null;
setFolderIdForSearch: (val: string | null) => void;
searchRecursively: boolean;
setSearchRecursively: (val: boolean) => void;
getBreadcrumbs: (dataset: APIMaybeUnimportedDataset) => string[] | null;
Expand Down Expand Up @@ -78,39 +78,29 @@ export default function DatasetCollectionContextProvider({
const [activeFolderId, setActiveFolderId] = useState<string | null>(
UserLocalStorage.getItem(ACTIVE_FOLDER_ID_STORAGE_KEY) || null,
);
const mostRecentlyUsedActiveFolderId = usePrevious(activeFolderId, true);
const [isChecking, setIsChecking] = useState(false);
const isMutating = useIsMutating() > 0;
const { data: folder } = useFolderQuery(activeFolderId);

const [globalSearchQuery, setGlobalSearchQueryInner] = useState<string | null>(null);
const [folderIdForSearch, setFolderIdForSearch] = useState<string | null>(null);
const setGlobalSearchQuery = useCallback(
(value: string | null) => {
// Empty string should be handled as null
setGlobalSearchQueryInner(value ? value : null);
if (value) {
setActiveFolderId(null);
}
},
[setGlobalSearchQueryInner, setActiveFolderId],
[setGlobalSearchQueryInner],
);
const [searchRecursively, setSearchRecursively] = useState<boolean>(true);

// Clear search query if active folder changes.
useEffect(() => {
if (activeFolderId != null) {
setGlobalSearchQuery(null);
}
}, [activeFolderId]);

// Keep url GET parameters in sync with search and active folder
useManagedUrlParams(
setGlobalSearchQuery,
setActiveFolderId,
globalSearchQuery,
activeFolderId,
folderIdForSearch,
setFolderIdForSearch,
searchRecursively,
setSearchRecursively,
);

useEffect(() => {
Expand All @@ -124,17 +114,21 @@ export default function DatasetCollectionContextProvider({
}, [folder, activeFolderId]);

const folderHierarchyQuery = useFolderHierarchyQuery();
const datasetsInFolderQuery = useDatasetsInFolderQuery(activeFolderId);
const datasetsInFolderQuery = useDatasetsInFolderQuery(
globalSearchQuery == null ? activeFolderId : null,
);
const datasetSearchQuery = useDatasetSearchQuery(
globalSearchQuery,
folderIdForSearch,
activeFolderId,
searchRecursively,
);
const createFolderMutation = useCreateFolderMutation();
const deleteFolderMutation = useDeleteFolderMutation();
const updateFolderMutation = useUpdateFolderMutation();
const moveFolderMutation = useMoveFolderMutation();
const updateDatasetMutation = useUpdateDatasetMutation(activeFolderId);
const updateDatasetMutation = useUpdateDatasetMutation(
globalSearchQuery == null ? activeFolderId : null,
);
const datasets = (globalSearchQuery ? datasetSearchQuery.data : datasetsInFolderQuery.data) || [];

async function fetchDatasets(_options: Options = {}): Promise<void> {
Expand Down Expand Up @@ -185,6 +179,7 @@ export default function DatasetCollectionContextProvider({
updateCachedDataset,
activeFolderId,
setActiveFolderId,
mostRecentlyUsedActiveFolderId,
isChecking,
getBreadcrumbs,
checkDatasets: async () => {
Expand All @@ -210,8 +205,6 @@ export default function DatasetCollectionContextProvider({
},
globalSearchQuery,
setGlobalSearchQuery,
folderIdForSearch,
setFolderIdForSearch,
searchRecursively,
setSearchRecursively,
queries: {
Expand All @@ -234,6 +227,7 @@ export default function DatasetCollectionContextProvider({
updateCachedDataset,
activeFolderId,
setActiveFolderId,
mostRecentlyUsedActiveFolderId,
folderHierarchyQuery,
datasetsInFolderQuery,
datasetSearchQuery,
Expand All @@ -245,8 +239,6 @@ export default function DatasetCollectionContextProvider({
moveFolderMutation,
updateDatasetMutation,
globalSearchQuery,
folderIdForSearch,
setFolderIdForSearch,
],
);

Expand All @@ -260,8 +252,8 @@ function useManagedUrlParams(
setActiveFolderId: React.Dispatch<React.SetStateAction<string | null>>,
globalSearchQuery: string | null,
activeFolderId: string | null,
folderIdForSearch: string | null,
setFolderIdForSearch: React.Dispatch<React.SetStateAction<string | null>>,
searchRecursively: boolean,
setSearchRecursively: (val: boolean) => void,
) {
const { data: folder } = useFolderQuery(activeFolderId);

Expand All @@ -274,8 +266,10 @@ function useManagedUrlParams(
}
const folderId = params.get("folderId");
if (folderId) {
setFolderIdForSearch(folderId);
setActiveFolderId(folderId);
}
const recursive = params.get("recursive");
setSearchRecursively(!!recursive);

const folderSpecifier = _.last(location.pathname.split("/"));

Expand All @@ -288,33 +282,11 @@ function useManagedUrlParams(
}
}, []);

// Update query and folderIdForSearch
useEffect(() => {
const params = new URLSearchParams(location.search);
if (globalSearchQuery) {
params.set("query", globalSearchQuery);
} else {
params.delete("query");
}
if (globalSearchQuery && folderIdForSearch) {
params.set("folderId", folderIdForSearch);
} else {
params.delete("folderId");
}
const paramStr = params.toString();

// Don't use useHistory because this would lose the input search
// focus.
window.history.replaceState(
{},
"",
`${location.pathname}${paramStr === "" ? "" : "?"}${paramStr}`,
);
}, [globalSearchQuery, folderIdForSearch]);
// Update query and searchRecursively

// Update folderId
useEffect(() => {
if (activeFolderId) {
if (!globalSearchQuery && activeFolderId) {
let folderName = folder?.name || "";
// The replacement of / and space is only done to make the URL
// nicer to read for a human.
Expand All @@ -332,7 +304,33 @@ function useManagedUrlParams(
`/dashboard/datasets/${folderName}${folderName ? "-" : ""}${activeFolderId}`,
);
} else {
window.history.replaceState({}, "", "/dashboard/datasets");
const params = new URLSearchParams(location.search);
if (globalSearchQuery) {
params.set("query", globalSearchQuery);
} else {
params.delete("query");
}
if (globalSearchQuery && activeFolderId) {
params.set("folderId", activeFolderId);
// The recursive property is only relevant when a folderId is specified.
if (searchRecursively) {
params.set("recursive", "true");
} else {
params.delete("recursive");
}
} else {
params.delete("folderId");
params.delete("recursive");
}
const paramStr = params.toString();

// Don't use useHistory because this would lose the input search
// focus.
window.history.replaceState(
{},
"",
`/dashboard/datasets${paramStr === "" ? "" : "?"}${paramStr}`,
);
}
}, [activeFolderId, folder]);
}, [globalSearchQuery, activeFolderId, folder, searchRecursively]);
}
9 changes: 8 additions & 1 deletion frontend/javascripts/dashboard/dataset/queries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,14 @@ export function useUpdateDatasetMutation(folderId: string | null) {
// has finished, the page will be complete).
return undefined;
}
return oldItems.concat([updatedDataset]);
return (
oldItems
// The dataset shouldn't be in oldItems, but if it should be
// for some reason (e.g., a bug), we filter it away to avoid
// duplicates.
.filter((el) => el.name !== updatedDataset.name)
.concat([updatedDataset])
);
},
);
}
Expand Down
11 changes: 8 additions & 3 deletions frontend/javascripts/dashboard/dataset_folder_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,15 @@ function DetailsSidebar({
if (selectedDataset == null || !("folderId" in selectedDataset)) {
return;
}
if (selectedDataset.folderId !== context.activeFolderId && context.activeFolderId != null) {
if (
selectedDataset.folderId !== context.activeFolderId &&
context.activeFolderId != null &&
context.globalSearchQuery == null
) {
// Ensure that the selected dataset is in the active folder. If not,
// clear the sidebar. When there is no active folder, a search page is shown. In that case,
// clearing the selection should not happen.
// clear the sidebar. Don't do this when search results are shown (since
// these can cover multiple folders).
// Typically, this is triggered when navigating to another folder.
setSelectedDataset(null);
}
}, [selectedDataset, context.activeFolderId]);
Expand Down
79 changes: 36 additions & 43 deletions frontend/javascripts/dashboard/dataset_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@ import {
Spin,
Tooltip,
Alert,
MenuProps,
TreeSelect,
Switch,
Checkbox,
Divider,
Select,
} from "antd";
import {
CloudUploadOutlined,
Expand Down Expand Up @@ -49,7 +45,7 @@ import { ActiveTabContext, RenderingTabContext } from "./dashboard_contexts";
import { DatasetCollectionContextValue } from "./dataset/dataset_collection_context";
import { MINIMUM_SEARCH_QUERY_LENGTH, SEARCH_RESULTS_LIMIT } from "./dataset/queries";

const { Search, Group: InputGroup } = Input;
const { Group: InputGroup } = Input;

type Props = {
user: APIUser;
Expand Down Expand Up @@ -88,7 +84,7 @@ function filterDatasetsForUsersOrganization(datasets: APIMaybeUnimportedDataset[
const refreshMenuItems = [
{
key: "1",
label: "Refresh from disk",
label: "Scan disk for new datasets",
},
];

Expand Down Expand Up @@ -218,7 +214,9 @@ function DatasetView(props: Props) {
/>
);
const searchBox = (
<Search
<Input
prefix={<SearchOutlined />}
allowClear
style={{
width: 200,
}}
Expand Down Expand Up @@ -320,6 +318,12 @@ function DatasetView(props: Props) {
);
}

const SEARCH_OPTIONS = [
{ label: "Search everywhere", value: "everywhere" },
{ label: "Search current folder", value: "folder" },
{ label: "Search current folder and its subfolders", value: "folder-with-subfolders" },
];

function GlobalSearchHeader({
searchQuery,
filteredDatasets,
Expand All @@ -333,11 +337,7 @@ function GlobalSearchHeader({
}) {
const { data: folderHierarchy } = context.queries.folderHierarchyQuery;
const [treeData, setTreeData] = useState<FolderItem[]>([]);
const { folderIdForSearch, setFolderIdForSearch } = context;

const onChange = (newValue: string) => {
setFolderIdForSearch(newValue);
};
const { activeFolderId, setActiveFolderId } = context;

useEffect(() => {
const newTreeData = folderHierarchy?.tree || [];
Expand All @@ -352,40 +352,33 @@ function GlobalSearchHeader({
<p>Enter at least {MINIMUM_SEARCH_QUERY_LENGTH} characters to search</p>
) : null;
}

return (
<>
<div style={{ float: "right" }}>
<Tooltip title="When selecting a folder, only that folder will be searched (not its child folders)">
<span style={{ marginRight: 4 }}>Where to search:</span>
</Tooltip>
<TreeSelect
size="small"
showSearch
style={{ width: 150 }}
value={folderIdForSearch || undefined}
dropdownStyle={{ maxHeight: 500, overflow: "auto" }}
placeholder="Everywhere"
allowClear
<Select
options={SEARCH_OPTIONS}
dropdownMatchSelectWidth={false}
treeDefaultExpandAll
onChange={onChange}
treeData={treeData}
fieldNames={{ label: "title", value: "key", children: "children" }}
treeNodeLabelProp="title"
dropdownRender={(node) => (
<div>
<div style={{ marginLeft: 4 }}>
<Checkbox
checked={context.searchRecursively}
onChange={() => context.setSearchRecursively(!context.searchRecursively)}
>
Also search subfolders
</Checkbox>
</div>
<Divider style={{ margin: "6px 0" }} />
{node}
</div>
)}
onChange={(value) => {
if (value === "everywhere") {
setActiveFolderId(null);
} else {
if (
activeFolderId == null &&
(context.mostRecentlyUsedActiveFolderId != null || treeData.length > 0)
) {
setActiveFolderId(context.mostRecentlyUsedActiveFolderId || treeData[0].key);
}
context.setSearchRecursively(value === "folder-with-subfolders");
}
}}
value={
activeFolderId == null
? "everywhere"
: context.searchRecursively
? "folder-with-subfolders"
: "folder"
}
/>
</div>
<h3>
Expand Down
Loading