-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: LEAP-1424: LEAP-1370: Allow to zoom and pan the image in previe…
…w modal (#6781) Allow users to zoom in and pan images in preview modal, which you can open from Grid View. Interactions are intuitive — scroll zooms and drag pans. This is also described in a tooltip. This PR also includes #6748 for LEAP-1424: Open a simple modal with image from the data of clicked task. Allow to navigate between tasks visible in Grid view by Prev/Next buttons. Only one image is displayed right now. Simple common hotkeys are available: - `<Arrows left/right>` to switch between tasks - `<Space>` to select/unselect task - `<Escape>` to close the modal Co-authored-by: bmartel <[email protected]> Co-authored-by: MihajloHoma <[email protected]> Co-authored-by: hlomzik <[email protected]>
- Loading branch information
1 parent
e4b519a
commit e5d54c7
Showing
6 changed files
with
505 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
62 changes: 62 additions & 0 deletions
62
web/libs/datamanager/src/components/MainView/GridView/GridPreview.module.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
.modal { | ||
padding: 16px; | ||
position: relative; | ||
} | ||
|
||
.header { | ||
display: flex; | ||
justify-content: space-between; | ||
align-items: center; | ||
box-sizing: content-box; | ||
margin-bottom: 16px; | ||
} | ||
|
||
.tooltip { | ||
font-size: 12px; | ||
max-width: 300px; | ||
line-height: 16px; | ||
|
||
p { | ||
margin-bottom: 4px; | ||
} | ||
|
||
p:last-child { | ||
margin-bottom: 0; | ||
} | ||
} | ||
|
||
.actions { | ||
margin-left: auto; | ||
|
||
& > * { | ||
width: 20px; | ||
margin-left: 16px; | ||
text-align: center; | ||
cursor: pointer; | ||
} | ||
} | ||
|
||
.container { | ||
overflow: hidden; | ||
width: 100%; | ||
height: 100%; | ||
min-height: 100px; | ||
display: flex; | ||
position: relative; | ||
} | ||
|
||
.container button { | ||
padding: 0; | ||
flex: 20px 0 0; | ||
cursor: pointer; | ||
background: none; | ||
|
||
&:hover { | ||
background: var(--sand_200); | ||
} | ||
} | ||
|
||
.image { | ||
pointer-events: none; | ||
user-select: none; | ||
} |
178 changes: 178 additions & 0 deletions
178
web/libs/datamanager/src/components/MainView/GridView/GridPreview.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
import { CloseOutlined, LeftCircleOutlined, QuestionCircleOutlined, RightCircleOutlined } from "@ant-design/icons"; | ||
import { Checkbox } from "@humansignal/ui"; | ||
import { observer } from "mobx-react"; | ||
import type { PropsWithChildren } from "react"; | ||
import { createContext, useCallback, useEffect, useRef, useState } from "react"; | ||
import { modal } from "../../Common/Modal/Modal"; | ||
import { Icon } from "../../Common/Icon/Icon"; | ||
import { Tooltip } from "../../Common/Tooltip/Tooltip"; | ||
import { ImagePreview } from "./ImagePreview"; | ||
|
||
import styles from "./GridPreview.module.scss"; | ||
|
||
type Task = { | ||
id: number; | ||
data: Record<string, string>; | ||
}; | ||
|
||
type GridViewContextType = { | ||
tasks: Task[]; | ||
imageField: string | undefined; | ||
currentTaskId: number | null; | ||
setCurrentTaskId: (id: number | null) => void; | ||
}; | ||
|
||
type TaskModalProps = GridViewContextType & { view: any; imageField: string }; | ||
|
||
export const GridViewContext = createContext<GridViewContextType>({ | ||
tasks: [], | ||
imageField: undefined, | ||
currentTaskId: null, | ||
setCurrentTaskId: () => {}, | ||
}); | ||
|
||
const TaskModal = observer(({ view, tasks, imageField, currentTaskId, setCurrentTaskId }: TaskModalProps) => { | ||
const index = tasks.findIndex((task) => task.id === currentTaskId); | ||
const task = tasks[index]; | ||
|
||
const goToNext = useCallback(() => { | ||
if (index < tasks.length - 1) { | ||
setCurrentTaskId(tasks[index + 1].id); | ||
} | ||
}, [index, tasks]); | ||
|
||
const goToPrev = useCallback(() => { | ||
if (index > 0) { | ||
setCurrentTaskId(tasks[index - 1].id); | ||
} | ||
}, [index, tasks]); | ||
|
||
const onSelect = useCallback(() => { | ||
if (task) { | ||
view.toggleSelected(task.id); | ||
} | ||
}, [task, view]); | ||
|
||
const onClose = useCallback(() => { | ||
setCurrentTaskId(null); | ||
}, []); | ||
|
||
// assign hotkeys | ||
useEffect(() => { | ||
const onKeyDown = (event: KeyboardEvent) => { | ||
if (event.key === "ArrowLeft") { | ||
goToPrev(); | ||
} else if (event.key === "ArrowRight") { | ||
goToNext(); | ||
} else if (event.key === " ") { | ||
onSelect(); | ||
event.preventDefault(); | ||
} else if (event.key === "Escape") { | ||
onClose(); | ||
} else { | ||
// pass this event through for other keys | ||
return; | ||
} | ||
|
||
event.stopPropagation(); | ||
}; | ||
|
||
document.addEventListener("keydown", onKeyDown); | ||
return () => document.removeEventListener("keydown", onKeyDown); | ||
}, [goToNext, goToPrev, onSelect, onClose]); | ||
|
||
if (!task) { | ||
return null; | ||
} | ||
|
||
const tooltip = ( | ||
<div className={styles.tooltip}> | ||
<p>Preview of the task image to quickly navigate through the tasks and select the ones you want to work on.</p> | ||
<p>Use [arrow keys] to navigate.</p> | ||
<p>[Escape] to close the modal.</p> | ||
<p>[Space] to select/unselect the task.</p> | ||
</div> | ||
); | ||
|
||
return ( | ||
<div className={styles.modal}> | ||
<div className={styles.header}> | ||
<Checkbox checked={view.selected.isSelected(task.id)} onChange={onSelect}> | ||
Task {task.id} | ||
</Checkbox> | ||
<div className={styles.actions}> | ||
<Tooltip title={tooltip}> | ||
<Icon icon={QuestionCircleOutlined} /> | ||
</Tooltip> | ||
<Icon icon={CloseOutlined} onClick={onClose} /> | ||
</div> | ||
</div> | ||
<div className={styles.container}> | ||
<button type="button" onClick={goToPrev} disabled={index === 0}> | ||
<Icon icon={LeftCircleOutlined} /> | ||
</button> | ||
<ImagePreview task={task} field={imageField} /> | ||
<button type="button" onClick={goToNext} disabled={index === tasks.length - 1}> | ||
<Icon icon={RightCircleOutlined} /> | ||
</button> | ||
</div> | ||
</div> | ||
); | ||
}); | ||
|
||
type GridViewProviderProps = PropsWithChildren<{ | ||
data: Task[]; | ||
view: any; | ||
fields: { alias: string; currentType: string }[]; | ||
}>; | ||
|
||
export const GridViewProvider: React.FC<GridViewProviderProps> = ({ children, data, view, fields }) => { | ||
const [currentTaskId, setCurrentTaskId] = useState<number | null>(null); | ||
const modalRef = useRef<{ update: (props: object) => void; close: () => void } | null>(null); | ||
const imageField = fields.find((f) => f.currentType === "Image")?.alias; | ||
|
||
const onClose = useCallback(() => { | ||
modalRef.current = null; | ||
setCurrentTaskId(null); | ||
}, []); | ||
|
||
useEffect(() => { | ||
if (currentTaskId === null) { | ||
modalRef.current?.close(); | ||
return; | ||
} | ||
|
||
if (!imageField) return; | ||
|
||
const children = ( | ||
<TaskModal | ||
view={view} | ||
tasks={data} | ||
imageField={imageField} | ||
currentTaskId={currentTaskId} | ||
setCurrentTaskId={setCurrentTaskId} | ||
/> | ||
); | ||
|
||
if (!modalRef.current) { | ||
modalRef.current = modal({ | ||
bare: true, | ||
title: "Task Preview", | ||
style: { width: 800 }, | ||
children, | ||
onHidden: onClose, | ||
}); | ||
} else { | ||
modalRef.current.update({ children }); | ||
} | ||
}, [currentTaskId, data, onClose]); | ||
|
||
// close the modal when we leave the view (by browser controls or by hotkeys) | ||
useEffect(() => () => modalRef.current?.close(), []); | ||
|
||
return ( | ||
<GridViewContext.Provider value={{ tasks: data, imageField, currentTaskId, setCurrentTaskId }}> | ||
{children} | ||
</GridViewContext.Provider> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.