-
Notifications
You must be signed in to change notification settings - Fork 30
View Annotations and Datasets from Command Palette #9087
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
base: master
Are you sure you want to change the base?
Changes from 20 commits
86271b7
1f0e069
f258f6b
c020f60
a6d8590
a5234eb
2fe142e
57fdf7b
3c4d137
feb58ae
b0cb4ac
4fd2ecb
65d749f
62984ae
c62998b
320114e
7353236
3994e16
6597d50
ca638cf
396a931
59d89c9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,22 @@ | ||
| import { updateSelectedThemeOfUser } from "admin/rest_api"; | ||
| import { getDatasets, getReadableAnnotations, updateSelectedThemeOfUser } from "admin/rest_api"; | ||
| import type { ItemType } from "antd/lib/menu/interface"; | ||
| import DOMPurify from "dompurify"; | ||
| import { useWkSelector } from "libs/react_hooks"; | ||
| import Toast from "libs/toast"; | ||
| import { capitalize, getPhraseFromCamelCaseString } from "libs/utils"; | ||
| import * as Utils from "libs/utils"; | ||
| import _ from "lodash"; | ||
| import { getAdministrationSubMenu } from "navbar"; | ||
| import { useMemo } from "react"; | ||
| import { useCallback, useMemo, useState } from "react"; | ||
| import type { Command } from "react-command-palette"; | ||
| import ReactCommandPalette from "react-command-palette"; | ||
| import { getSystemColorTheme, getThemeFromUser } from "theme"; | ||
| import { WkDevFlags } from "viewer/api/wk_dev"; | ||
| import { ViewModeValues } from "viewer/constants"; | ||
| import { getViewDatasetURL } from "viewer/model/accessors/dataset_accessor"; | ||
| import { AnnotationTool } from "viewer/model/accessors/tool_accessor"; | ||
| import { Toolkits } from "viewer/model/accessors/tool_accessor"; | ||
| import { updateUserSettingAction } from "viewer/model/actions/settings_actions"; | ||
| import { setViewModeAction, updateUserSettingAction } from "viewer/model/actions/settings_actions"; | ||
| import { setThemeAction, setToolAction } from "viewer/model/actions/ui_actions"; | ||
| import { setActiveUserAction } from "viewer/model/actions/user_actions"; | ||
| import { Store } from "viewer/singletons"; | ||
|
|
@@ -22,11 +26,18 @@ | |
| useTracingViewMenuItems, | ||
| } from "../action-bar/use_tracing_view_menu_items"; | ||
| import { viewDatasetMenu } from "../action-bar/view_dataset_actions_view"; | ||
| import { LayoutEvents, layoutEmitter } from "../layouting/layout_persistence"; | ||
| import { commandPaletteDarkTheme, commandPaletteLightTheme } from "./command_palette_theme"; | ||
|
|
||
| const commandEntryColor = "#5660ff"; | ||
|
|
||
| // duplicate fields because otherwise, optional fields of Command yield errors | ||
| type CommandWithoutId = Omit<Command, "id">; | ||
|
|
||
| const commandEntryColor = "#5660ff"; | ||
| enum DynamicCommands { | ||
| viewDataset = "View Dataset ", | ||
| viewAnnotation = "View Annotation ", | ||
| } | ||
|
|
||
| const getLabelForAction = (action: NonNullable<ItemType>) => { | ||
| if ("title" in action && action.title != null) { | ||
|
|
@@ -57,6 +68,11 @@ | |
| const getLabelForPath = (key: string) => | ||
| getPhraseFromCamelCaseString(capitalize(key.split("/")[1])) || key; | ||
|
|
||
| const cleanStringOfMostHTML = (dirtyString: string | undefined) => { | ||
| if (dirtyString == null) return null; | ||
| return DOMPurify.sanitize(dirtyString, { ALLOWED_TAGS: ["b"] }); | ||
| }; | ||
|
|
||
| export const CommandPalette = ({ label }: { label: string | JSX.Element | null }) => { | ||
| const userConfig = useWkSelector((state) => state.userConfiguration); | ||
| const isViewMode = useWkSelector((state) => state.temporaryConfiguration.controlMode === "VIEW"); | ||
|
|
@@ -99,6 +115,83 @@ | |
| return commands; | ||
| }; | ||
|
|
||
| const handleSelect = useCallback(async (command: Record<string, unknown>) => { | ||
| if (typeof command === "string") { | ||
| return; | ||
| } | ||
|
|
||
| if (command.name === DynamicCommands.viewDataset) { | ||
| try { | ||
| const items = await getDatasetItems(); | ||
| if (items.length > 0) { | ||
| setCommands(items); | ||
| } else { | ||
| Toast.info("No datasets available."); | ||
| } | ||
| } catch (_e) { | ||
| Toast.error("Failed to load datasets."); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| if (command.name === DynamicCommands.viewAnnotation) { | ||
| try { | ||
| const items = await getAnnotationItems(); | ||
| if (items.length > 0) { | ||
| setCommands(items); | ||
| } else { | ||
| Toast.info("No annotations available."); | ||
| } | ||
| } catch (_e) { | ||
| Toast.error("Failed to load annotations."); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| closePalette(); | ||
| }, []); | ||
|
|
||
| const getDatasetItems = useCallback(async () => { | ||
| const datasets = await getDatasets(); | ||
| return datasets.map((dataset) => ({ | ||
| name: `View Dataset ${dataset.name} (id ${dataset.id})`, | ||
| command: () => { | ||
| window.location.href = getViewDatasetURL(dataset); | ||
| }, | ||
| color: commandEntryColor, | ||
| id: dataset.id, | ||
| })); | ||
| }, []); | ||
|
|
||
| const viewDatasetsItem = { | ||
| name: DynamicCommands.viewDataset, | ||
| command: () => {}, | ||
| shortcut: "Enter to show list", | ||
| color: commandEntryColor, | ||
| }; | ||
|
|
||
| const getAnnotationItems = useCallback(async () => { | ||
| const annotations = await getReadableAnnotations(false); | ||
| const sortedAnnotations = _.sortBy(annotations, (a) => a.modified).reverse(); | ||
| return sortedAnnotations.map((annotation) => { | ||
| return { | ||
| name: `View Annotation ${annotation.name.length > 0 ? `${annotation.name} (id ${annotation.id})` : annotation.id}`, | ||
| command: () => { | ||
| window.location.href = `/annotations/${annotation.id}`; | ||
| }, | ||
| color: commandEntryColor, | ||
| id: annotation.id, | ||
| }; | ||
| }); | ||
| }, []); | ||
|
|
||
| const viewAnnotationItems = { | ||
| name: DynamicCommands.viewAnnotation, | ||
| shortcut: "Enter to show list", | ||
| command: () => {}, | ||
| color: commandEntryColor, | ||
| }; | ||
|
|
||
| const getSuperUserItems = (): CommandWithoutId[] => { | ||
| if (!activeUser?.isSuperUser) { | ||
| return []; | ||
|
|
@@ -185,6 +278,36 @@ | |
| return commands; | ||
| }; | ||
|
|
||
| const getViewModeEntries = () => { | ||
| if (!isInTracingView) return []; | ||
| const commands = ViewModeValues.map((mode) => ({ | ||
| name: `Switch to ${mode} mode`, | ||
| command: () => { | ||
| Store.dispatch(setViewModeAction(mode)); | ||
| }, | ||
| color: commandEntryColor, | ||
| })); | ||
| commands.push({ | ||
| name: "Reset layout", | ||
| command: () => layoutEmitter.emit(LayoutEvents.resetLayout), | ||
| color: commandEntryColor, | ||
| }); | ||
| return commands; | ||
| }; | ||
|
|
||
| const shortCutDictForTools: Record<string, string> = { | ||
| [AnnotationTool.MOVE.id]: "Ctrl + K, M", | ||
| [AnnotationTool.SKELETON.id]: "Ctrl + K, S", | ||
| [AnnotationTool.BRUSH.id]: "Ctrl + K, B", | ||
| [AnnotationTool.ERASE_BRUSH.id]: "Ctrl + K, E", | ||
| [AnnotationTool.TRACE.id]: "Ctrl + K, L", | ||
| [AnnotationTool.ERASE_TRACE.id]: "Ctrl + K, R", | ||
| [AnnotationTool.VOXEL_PIPETTE.id]: "Ctrl + K, P", | ||
| [AnnotationTool.QUICK_SELECT.id]: "Ctrl + K, Q", | ||
| [AnnotationTool.BOUNDING_BOX.id]: "Ctrl + K, X", | ||
| [AnnotationTool.PROOFREAD.id]: "Ctrl + K, O", | ||
| }; | ||
knollengewaechs marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const getToolEntries = () => { | ||
| if (!isInTracingView) return []; | ||
| const commands: CommandWithoutId[] = []; | ||
|
|
@@ -196,6 +319,7 @@ | |
| commands.push({ | ||
| name: `Switch to ${tool.readableName}`, | ||
| command: () => Store.dispatch(setToolAction(tool)), | ||
| shortcut: shortCutDictForTools[tool.id] || "", | ||
|
Check failure on line 322 in frontend/javascripts/viewer/view/components/command_palette.tsx
|
||
| color: commandEntryColor, | ||
| }); | ||
| }); | ||
|
|
@@ -212,28 +336,62 @@ | |
| return tracingMenuItems; | ||
| }, [isInTracingView, isViewMode, tracingMenuItems]); | ||
|
|
||
| const allCommands = [ | ||
| const allStaticCommands = [ | ||
| viewDatasetsItem, | ||
| viewAnnotationItems, | ||
| ...getNavigationEntries(), | ||
| ...getThemeEntries(), | ||
| ...getToolEntries(), | ||
| ...getViewModeEntries(), | ||
| ...mapMenuActionsToCommands(menuActions), | ||
| ...getTabsAndSettingsMenuItems(), | ||
| ...getSuperUserItems(), | ||
| ]; | ||
|
|
||
| const [commands, setCommands] = useState<CommandWithoutId[]>(allStaticCommands); | ||
| const [paletteKey, setPaletteKey] = useState(0); | ||
|
|
||
| const closePalette = () => { | ||
| setPaletteKey((prevKey) => prevKey + 1); | ||
| }; | ||
|
|
||
knollengewaechs marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const commandsWithIds = useMemo(() => { | ||
| return commands.map((command, index) => { | ||
| return { id: index, ...command }; | ||
| }); | ||
| }, [commands]); | ||
|
|
||
| return ( | ||
| <ReactCommandPalette | ||
| commands={allCommands.map((command, counter) => { | ||
| return { | ||
| ...command, | ||
| id: counter, | ||
| }; | ||
| })} | ||
| commands={commandsWithIds} | ||
| key={paletteKey} | ||
| hotKeys={["ctrl+p", "command+p"]} | ||
| trigger={label} | ||
| closeOnSelect | ||
| resetInputOnOpen | ||
| maxDisplayed={100} | ||
| theme={theme === "light" ? commandPaletteLightTheme : commandPaletteDarkTheme} | ||
| onSelect={handleSelect} | ||
| showSpinnerOnSelect={false} | ||
| resetInputOnOpen | ||
| onRequestClose={() => setCommands(allStaticCommands)} | ||
| closeOnSelect={false} | ||
| renderCommand={(command) => { | ||
| const { shortcut, highlight: maybeDirtyString, name } = command; | ||
|
Check failure on line 378 in frontend/javascripts/viewer/view/components/command_palette.tsx
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @philippotto do you have any idea why this fails and how to fix it? no matter if use the import form
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| const cleanString = cleanStringOfMostHTML(maybeDirtyString); | ||
| return ( | ||
| <div | ||
| className="item" | ||
| style={{ display: "flex", justifyContent: "space-between", width: "100%" }} | ||
| > | ||
| {cleanString ? ( | ||
| // biome-ignore lint/security/noDangerouslySetInnerHtml: modified from https://github.com/asabaylus/react-command-palette/blob/main/src/default-command.js | ||
| <span dangerouslySetInnerHTML={{ __html: cleanString }} /> | ||
| ) : ( | ||
| <span>{name}</span> | ||
| )} | ||
| <span>{shortcut}</span> | ||
| </div> | ||
| ); | ||
| }} | ||
| /> | ||
| ); | ||
| }; | ||

Uh oh!
There was an error while loading. Please reload this page.