diff --git a/package.json b/package.json index c1a99635e..47f83cf37 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "allusion", "productName": "Allusion", - "version": "1.0.0-rc6", + "version": "1.0.0-rc6.1", "description": "A tool for managing your visual library", "main": "build/main.bundle.js", "scripts": { @@ -90,7 +90,7 @@ "@typescript-eslint/eslint-plugin": "^4.2.0", "@typescript-eslint/parser": "^4.2.0", "css-loader": "^5.2.0", - "electron": "15.0.0", + "electron": "^15.3.3", "electron-builder": "^22.10.5", "eslint": "^7.18.0", "eslint-config-prettier": "^8.1.0", diff --git a/resources/style/inspector.scss b/resources/style/inspector.scss index a912aee63..bd97cb88a 100644 --- a/resources/style/inspector.scss +++ b/resources/style/inspector.scss @@ -1,10 +1,23 @@ ///////////////////////////////// Inspector ///////////////////////////////// #inspector { background-color: var(--background-color); - overflow: hidden; + overflow: hidden auto; // show a Y scrollbar if inspector is too small in height to fit min-height of its content + + display: flex; + flex-direction: column; > * { padding: 0.25rem 0.5rem; + + // Tags section should shrink to fit its parent if the inspector is small in height + &:last-child { + display: flex; + flex-direction: column; + flex: 1; + overflow: hidden; + // so that the tags section remains visible when inspector is very small in height: inspector should overflow with a scrollbar + min-height: 6rem; + } } header { diff --git a/resources/style/remake/outliner.scss b/resources/style/remake/outliner.scss index e4fec3a90..ed491b860 100644 --- a/resources/style/remake/outliner.scss +++ b/resources/style/remake/outliner.scss @@ -97,9 +97,11 @@ .tree-content-label { display: flex; + flex: 1 1 auto; align-items: center; height: 1.5rem; width: 100%; + overflow: hidden; white-space: nowrap; span { @@ -148,10 +150,10 @@ } > .label-text { + flex: 1; text-overflow: ellipsis; white-space: nowrap; overflow: clip visible; - width: 100%; line-height: 1; padding-bottom: 0px; } diff --git a/resources/style/remake/tag-editor.scss b/resources/style/remake/tag-editor.scss index 036a402ef..92896e980 100644 --- a/resources/style/remake/tag-editor.scss +++ b/resources/style/remake/tag-editor.scss @@ -15,14 +15,25 @@ overflow: auto; resize: vertical; + // Intended layout behavior: + // - Input element has a static height + // - The tag checklist grows to fit the available space, until a max-height is reached + // - The applied tags section has a static height of 1.5 rows of tags, + // and grows when the tag check-list has reached max-height + // TODO: preferably: this section first fits to its content up to 3.5 rows (so only 1 row if there is 1 row of tags) + // could figure out the CSS: we need something like min-height: calc(max(1.5rem, min(fit-content, 3.5rem))) + // maybe with the fit-content() property coming soon? https://developer.mozilla.org/en-US/docs/Web/CSS/min-height#:~:text=5.0-,fit%2Dcontent(),-Experimental + > input { margin: 0.5rem; width: auto; height: auto; + min-height: fit-content; // prevents weird slight resize when checklist shrinks when resizing parent } + // Checklist of all available tags [role='grid'] { - flex: 1; + flex: 0 1 calc(16rem + 1px); // expand to fill available space, don't shrink unless absolutaly necessary border-top: 0.0625rem solid var(--border-color); border-bottom: 0.0625rem solid var(--border-color); max-width: unset; @@ -34,14 +45,17 @@ } } + // The tags applied to the selected images > div:last-child { + flex: 1 1 auto; // shrink to make room for other elements, but grow if there is space available (other other elements reached their max height) margin: 0.5rem; - min-height: 1.375rem; - max-height: 3.5rem; + height: 3.375rem; + min-height: 2.375rem; + // preferably: min-height calc(max(min(3.75rem, fit-content), 1.375rem)), but doesn't work overflow: auto; display: flex; flex-wrap: wrap; - align-items: center; + align-content: flex-start; } } diff --git a/src/Messaging.ts b/src/Messaging.ts index 1c9ab58cd..3cccde636 100644 --- a/src/Messaging.ts +++ b/src/Messaging.ts @@ -32,6 +32,7 @@ const TOGGLE_DEV_TOOLS = 'TOGGLE_DEV_TOOLS'; const RELOAD = 'RELOAD'; const OPEN_DIALOG = 'OPEN_DIALOG'; const GET_PATH = 'GET_PATH'; +const TRASH_FILE = 'TRASH_FILE'; const SET_FULL_SCREEN = 'SET_FULL_SCREEN'; const IS_FULL_SCREEN = 'IS_FULL_SCREEN'; const FULL_SCREEN_CHANGED = 'FULL_SCREEN_CHANGED'; @@ -137,6 +138,9 @@ export class RendererMessenger { static getPath = (name: SYSTEM_PATHS): Promise => ipcRenderer.invoke(GET_PATH, name); + static trashFile = (absolutePath: string): Promise => + ipcRenderer.invoke(TRASH_FILE, absolutePath); + static setFullScreen = (isFullScreen: boolean) => ipcRenderer.invoke(SET_FULL_SCREEN, isFullScreen); @@ -235,6 +239,15 @@ export class MainMessenger { static onGetPath = (cb: (name: SYSTEM_PATHS) => string) => ipcMain.handle(GET_PATH, (_, name) => cb(name)); + static onTrashFile = (cb: (absolutePath: string) => Promise) => + ipcMain.handle(TRASH_FILE, async (_, absolutePath) => { + try { + await cb(absolutePath); + } catch (e) { + return e; + } + }); + static onSetFullScreen = (cb: (isFullScreen: boolean) => void) => ipcMain.handle(SET_FULL_SCREEN, (_, isFullScreen) => cb(isFullScreen)); diff --git a/src/frontend/components/FileTag.tsx b/src/frontend/components/FileTag.tsx index 94325e23a..61597cc1e 100644 --- a/src/frontend/components/FileTag.tsx +++ b/src/frontend/components/FileTag.tsx @@ -5,6 +5,10 @@ import { IconSet } from 'widgets/Icons'; import { Row } from 'widgets'; import { useStore } from '../contexts/StoreContext'; import { TagSelector } from './TagSelector'; +import useContextMenu from '../hooks/useContextMenu'; +import { FileTagMenuItems } from '../containers/ContentView/menu-items'; +import { ClientTag } from 'src/entities/Tag'; +import { ContextMenu, Menu } from 'widgets/menus'; interface IFileTagProp { file: ClientFile; @@ -13,6 +17,8 @@ interface IFileTagProp { const FileTags = observer(({ file }: IFileTagProp) => { const { tagStore } = useStore(); + const [contextState, { show, hide }] = useContextMenu(); + const renderCreateOption = useCallback( (tagName: string, resetTextBox: () => void) => ( { [file, tagStore], ); + const handleTagContextMenu = useCallback( + (event: React.MouseEvent, tag: ClientTag) => { + event.stopPropagation(); + show(event.clientX, event.clientY, [ + + + , + ]); + }, + [file, show], + ); + return ( - + <> + + + {/* TODO: probably not the right place for the ContextMenu component. + Why not a single one at the root element that can be interacted with through a Context? */} + + {contextState.menu} + + ); }); diff --git a/src/frontend/components/RemovalAlert.tsx b/src/frontend/components/RemovalAlert.tsx index 6b49d74eb..b85c854c5 100644 --- a/src/frontend/components/RemovalAlert.tsx +++ b/src/frontend/components/RemovalAlert.tsx @@ -7,7 +7,8 @@ import { ClientTag } from 'src/entities/Tag'; import { useStore } from 'src/frontend/contexts/StoreContext'; import { Tag, IconSet } from 'widgets'; import { Alert, DialogButton } from 'widgets/popovers'; -import { shell } from 'electron'; +import { AppToaster } from './Toaster'; +import { RendererMessenger } from 'src/Messaging'; interface IRemovalProps { object: T; @@ -124,14 +125,27 @@ export const MoveFilesToTrashBin = observer(() => { uiStore.closeMoveFilesToTrash(); const files = []; for (const file of selection) { - try { - await shell.trashItem(file.absolutePath); + // File deletion used to be possible in renderer process, not in new electron version + // await shell.trashItem(file.absolutePath); + // https://github.com/electron/electron/issues/29598 + const error = await RendererMessenger.trashFile(file.absolutePath); + if (!error) { files.push(file); - } catch (error) { - console.warn('Could not move file to trash', file.absolutePath); + } else { + console.warn('Could not move file to trash', file.absolutePath, error); } } fileStore.deleteFiles(files); + if (files.length !== selection.size) { + AppToaster.show({ + message: 'Some files could not be deleted.', + clickAction: { + onClick: () => RendererMessenger.toggleDevTools(), + label: 'More info', + }, + timeout: 8000, + }); + } }); const isMulti = selection.size > 1; diff --git a/src/frontend/components/TagSelector.tsx b/src/frontend/components/TagSelector.tsx index 1d0e9c4f4..d8ed17e53 100644 --- a/src/frontend/components/TagSelector.tsx +++ b/src/frontend/components/TagSelector.tsx @@ -21,6 +21,8 @@ export interface TagSelectorProps { inputText: string, resetTextBox: () => void, ) => ReactElement | ReactElement[]; + multiline?: boolean; + showTagContextMenu?: (e: React.MouseEvent, tag: ClientTag) => void; } const TagSelector = (props: TagSelectorProps) => { @@ -29,10 +31,12 @@ const TagSelector = (props: TagSelectorProps) => { onSelect, onDeselect, onTagClick, + showTagContextMenu, onClear, disabled, extraIconButtons, renderCreateOption, + multiline, } = props; const gridId = useRef(generateWidgetId('__suggestions')).current; const inputRef = useRef(null); @@ -86,6 +90,8 @@ const TagSelector = (props: TagSelectorProps) => { const handleFocus = useRef(() => setIsOpen(true)).current; + const handleBackgroundClick = useCallback(() => inputRef.current?.focus(), []); + const resetTextBox = useRef(() => { inputRef.current?.focus(); setQuery(''); @@ -109,8 +115,9 @@ const TagSelector = (props: TagSelectorProps) => { aria-expanded={isOpen} aria-haspopup="grid" aria-owns={gridId} - className="input multiautocomplete tag-selector" + className={`input multiautocomplete tag-selector ${multiline ? 'multiline' : ''}`} onBlur={handleBlur} + onClick={handleBackgroundClick} > {
{selection.map((t) => ( - + ))} void; onTagClick?: (item: ClientTag) => void; + showContextMenu?: (e: React.MouseEvent, item: ClientTag) => void; } const SelectedTag = observer((props: SelectedTagProps) => { - const { tag, onDeselect, onTagClick } = props; + const { tag, onDeselect, onTagClick, showContextMenu } = props; return ( onDeselect(tag)} onClick={onTagClick !== undefined ? () => onTagClick(tag) : undefined} + onContextMenu={showContextMenu !== undefined ? (e) => showContextMenu(e, tag) : undefined} /> ); }); diff --git a/src/frontend/containers/AppToolbar/FileTagEditor.tsx b/src/frontend/containers/AppToolbar/FileTagEditor.tsx index c94fdc429..67cbd6436 100644 --- a/src/frontend/containers/AppToolbar/FileTagEditor.tsx +++ b/src/frontend/containers/AppToolbar/FileTagEditor.tsx @@ -110,6 +110,29 @@ const TagEditor = () => { inputRef.current?.focus(); }); + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Backspace') { + // Prevent backspace from navigating back to main view when having an image open + e.stopPropagation(); + } + + if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') { + // If shift key is pressed with arrow keys left/right, + // stop those key events from propagating to the gallery, + // so that the cursor in the text input can be moved without selecting the prev/next image + // Kind of an ugly work-around, but better than not being able to move the cursor at all + if (e.shiftKey) { + e.stopPropagation(); // move text cursor as expected (and select text because shift is pressed) + } else { + e.preventDefault(); // don't do anything here: let the event propagate to the gallery + } + } + handleGridFocus(e); + }, + [handleGridFocus], + ); + return (
{ value={inputText} aria-autocomplete="list" onChange={handleInput} - onKeyDown={handleGridFocus} + onKeyDown={handleKeyDown} className="input" aria-controls={POPUP_ID} aria-activedescendant={activeDescendant} @@ -279,7 +302,7 @@ const FloatingPanel = observer(({ children }: { children: ReactNode }) => { } }).current; - const handleClose = useRef((e: React.KeyboardEvent) => { + const handleKeyDown = useRef((e: React.KeyboardEvent) => { if (e.key === 'Escape') { e.stopPropagation(); uiStore.closeToolbarTagPopover(); @@ -294,7 +317,7 @@ const FloatingPanel = observer(({ children }: { children: ReactNode }) => { data-open={uiStore.isToolbarTagPopoverOpen} className="floating-dialog" onBlur={handleBlur} - onKeyDown={handleClose} + onKeyDown={handleKeyDown} > {uiStore.isToolbarTagPopoverOpen ? children : null}
diff --git a/src/frontend/containers/ContentView/LayoutSwitcher.tsx b/src/frontend/containers/ContentView/LayoutSwitcher.tsx index ad5e09754..9a1869971 100644 --- a/src/frontend/containers/ContentView/LayoutSwitcher.tsx +++ b/src/frontend/containers/ContentView/LayoutSwitcher.tsx @@ -89,12 +89,13 @@ const Layout = ({ contentRect, showContextMenu }: LayoutProps) => { } if (e.key === 'ArrowLeft' && index > 0) { index -= 1; - // TODO: when the activeElement GalleryItem goes out of view, focus will be handed over to the body element: - // -> Gallery keyboard shortkeys stop working. So, force focus on container - FocusManager.focusGallery(); + // When the activeElement GalleryItem goes out of view, focus will be handed over to the body element: + // -> Gallery keyboard shortkeys stop working. So, force focus on Gallery container instead + // But not when the TagEditor overlay is open: it will close onBlur + if (!uiStore.isToolbarTagPopoverOpen) FocusManager.focusGallery(); } else if (e.key === 'ArrowRight' && index < fileStore.fileList.length - 1) { index += 1; - FocusManager.focusGallery(); + if (!uiStore.isToolbarTagPopoverOpen) FocusManager.focusGallery(); } else { return; } diff --git a/src/frontend/containers/ContentView/Masonry/MasonryRenderer.tsx b/src/frontend/containers/ContentView/Masonry/MasonryRenderer.tsx index 1ed8f8665..8e07770c4 100644 --- a/src/frontend/containers/ContentView/Masonry/MasonryRenderer.tsx +++ b/src/frontend/containers/ContentView/Masonry/MasonryRenderer.tsx @@ -75,13 +75,18 @@ const MasonryRenderer = observer(({ contentRect, select, lastSelectionIndex }: G } e.preventDefault(); select(fileStore.fileList[index], e.ctrlKey || e.metaKey, e.shiftKey); - FocusManager.focusGallery(); + + // Don't change focus when TagEditor overlay is open: is closes onBlur + if (!uiStore.isToolbarTagPopoverOpen) { + FocusManager.focusGallery(); + } }); const throttledKeyDown = throttle(onKeyDown, 50); window.addEventListener('keydown', throttledKeyDown); return () => window.removeEventListener('keydown', throttledKeyDown); - }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); // Initialize on mount useEffect(() => { diff --git a/src/frontend/containers/ContentView/SlideMode/index.tsx b/src/frontend/containers/ContentView/SlideMode/index.tsx index 9b3f8f305..f62879fd7 100644 --- a/src/frontend/containers/ContentView/SlideMode/index.tsx +++ b/src/frontend/containers/ContentView/SlideMode/index.tsx @@ -55,7 +55,12 @@ const SlideView = observer(({ width, height }: SlideViewProps) => { () => uiStore.firstSelectedFile?.id, (id, _, reaction) => { if (id !== undefined) { - uiStore.setFirstItem(fileStore.getIndex(id)); + const index = fileStore.getIndex(id); + uiStore.setFirstItem(index); + + // Also, select only this file: makes more sense for the TagEditor overlay: shows tags on selected images + if (index !== undefined) uiStore.selectFile(fileStore.fileList[index], true); + reaction.dispose(); } }, @@ -66,16 +71,25 @@ const SlideView = observer(({ width, height }: SlideViewProps) => { // Go back to previous view when pressing the back button (mouse button 5) useEffect(() => { // Push a dummy state, so that a pop-state event can be activated + // TODO: would be nice to also open SlideMode again when pressing forward button: actually store the open image in the window.location? history.pushState(null, document.title, location.href); const popStateHandler = uiStore.disableSlideMode; window.addEventListener('popstate', popStateHandler); return () => window.removeEventListener('popstate', popStateHandler); }, [uiStore]); - const decrImgIndex = useAction(() => uiStore.setFirstItem(Math.max(0, uiStore.firstItem - 1))); - const incrImgIndex = useAction(() => - uiStore.setFirstItem(Math.min(uiStore.firstItem + 1, fileStore.fileList.length - 1)), - ); + const decrImgIndex = useAction(() => { + const index = Math.max(0, uiStore.firstItem - 1); + uiStore.setFirstItem(index); + + // Select only this file: TagEditor overlay shows tags on selected images + uiStore.selectFile(fileStore.fileList[index], true); + }); + const incrImgIndex = useAction(() => { + const index = Math.min(uiStore.firstItem + 1, fileStore.fileList.length - 1); + uiStore.setFirstItem(); + uiStore.selectFile(fileStore.fileList[index], true); + }); // Detect left/right arrow keys to scroll between images. Top/down is already handled in the layout that's open in the background useEffect(() => { diff --git a/src/frontend/containers/Outliner/TagsPanel/TagsTree.tsx b/src/frontend/containers/Outliner/TagsPanel/TagsTree.tsx index 2866b1e31..14eb0e872 100644 --- a/src/frontend/containers/Outliner/TagsPanel/TagsTree.tsx +++ b/src/frontend/containers/Outliner/TagsPanel/TagsTree.tsx @@ -72,6 +72,8 @@ const Label = (props: ILabelProps) => } }} onFocus={(e) => e.target.select()} + // Stop propagation so that the parent Tag element doesn't toggle selection status + onClick={(e) => e.stopPropagation()} // TODO: Visualizing errors... // Only show red outline when input field is in focus and text is invalid /> diff --git a/src/frontend/stores/UiStore.ts b/src/frontend/stores/UiStore.ts index aea68aa90..8b112b4d1 100644 --- a/src/frontend/stores/UiStore.ts +++ b/src/frontend/stores/UiStore.ts @@ -60,7 +60,7 @@ export const defaultHotkeyMap: IHotkeyMap = { openTagEditor: 't', selectAll: 'mod + a', deselectAll: 'mod + d', - viewSlide: 'alt + 0', + viewSlide: 'enter', // TODO: backspace and escape are hardcoded hotkeys to exist slide mode viewList: 'alt + 1', viewGrid: 'alt + 2', viewMasonryVertical: 'alt + 3', diff --git a/src/frontend/workers/folderWatcher.worker.ts b/src/frontend/workers/folderWatcher.worker.ts index 3bcf3b785..6708e5a2e 100644 --- a/src/frontend/workers/folderWatcher.worker.ts +++ b/src/frontend/workers/folderWatcher.worker.ts @@ -90,15 +90,17 @@ export class FolderWatcherWorker { const ext = SysPath.extname(path).toLowerCase().split('.')[1]; if (extensions.includes(ext as IMG_EXTENSIONS_TYPE)) { + const fileStats: FileStats = { + absolutePath: path, + dateCreated: stats?.birthtime, + dateModified: stats?.mtime, + size: stats?.size, + }; + if (this.isReady) { - ctx.postMessage({ type: 'add', value: path }); + ctx.postMessage({ type: 'add', value: fileStats }); } else { - initialFiles.push({ - absolutePath: path, - dateCreated: stats?.birthtime, - dateModified: stats?.mtime, - size: stats?.size, - }); + initialFiles.push(fileStats); } } }) @@ -109,10 +111,10 @@ export class FolderWatcherWorker { this.isReady = true; resolve(initialFiles); - // TODO: Clear memory: initialFiles no longer needed - // Update: tried it, didn't work as expected: list was emptied before sent back to main thread - // maybe send a message from main thread after initialization is finished instead? - // initialFiles.splice(0, initialFiles.length); + // Clear memory: initialFiles no longer needed + // Doing this immediately after resolving will resolve with an empty list for some reason + // So, do it with a timeout. Would be nicer to do it after an acknowledgement from the main thread + setTimeout(() => initialFiles.splice(0, initialFiles.length), 5000); }) .on('error', (error) => { console.error('Error fired in watcher', directory, error); diff --git a/src/main.ts b/src/main.ts index ad38b8348..ca8c51e92 100644 --- a/src/main.ts +++ b/src/main.ts @@ -520,6 +520,9 @@ autoUpdater.on('update-not-available', () => { }); autoUpdater.on('update-downloaded', async () => { + if (mainWindow !== null && !mainWindow.isDestroyed()) { + mainWindow.setProgressBar(10); // indeterminate mode until application is restarted + } await dialog.showMessageBox({ title: 'Install Updates', message: 'Updates downloaded, Allusion will restart...', @@ -527,6 +530,53 @@ autoUpdater.on('update-downloaded', async () => { setImmediate(() => autoUpdater.quitAndInstall()); }); +// Show the auto-update download progress in the task bar +autoUpdater.on('download-progress', (progressObj: { percent: number }) => { + if (mainWindow === null || mainWindow.isDestroyed()) { + return; + } + if (tray && !tray.isDestroyed()) { + tray.setToolTip(`Allusion - Downloading update ${progressObj.percent.toFixed(0)}%`); + } + // TODO: could also do this for other tasks (e.g. importing folders) + mainWindow.setProgressBar(progressObj.percent / 100); +}); + +// Handling uncaught exceptions: +process.on('uncaughtException', async (error) => { + console.error('Uncaught exception', error); + + const errorMessage = `An unexpected error occurred. Please file a bug report if you think this needs fixing!\n${ + error?.stack?.includes(error.message) ? '' : `${error.name}: ${error.message.slice(0, 200)}\n` + }\n${error.stack?.slice(0, 300)}`; + + try { + if (mainWindow != null && !mainWindow.isDestroyed()) { + // Show a dialog prompting the user to either restart, continue on or quit + const dialogResult = await dialog.showMessageBox(mainWindow, { + type: 'error', + title: 'Unexpected error', + message: errorMessage, + buttons: ['Restart Allusion', 'Quit Allusion', 'Try to keep running'], + }); + if (dialogResult.response === 0) { + forceRelaunch(); // Restart + } else if (dialogResult.response === 1) { + app.exit(0); // Quit + } else if (dialogResult.response === 2) { + // Keep running + } + } else { + // No main window, show a fallback dialog + dialog.showErrorBox('Unexpected error', errorMessage); + app.exit(1); + } + } catch (e) { + console.error('Could not show error dialog', e); + process.exit(1); + } +}); + //---------------------------------------------------------------------------------// // Messaging: Sending and receiving messages between the main and renderer process // //---------------------------------------------------------------------------------// @@ -628,6 +678,8 @@ MainMessenger.onOpenDialog(dialog); MainMessenger.onGetPath((path) => app.getPath(path)); +MainMessenger.onTrashFile((absolutePath) => shell.trashItem(absolutePath)); + MainMessenger.onIsFullScreen(() => mainWindow?.isFullScreen() ?? false); MainMessenger.onSetFullScreen((isFullScreen) => mainWindow?.setFullScreen(isFullScreen)); diff --git a/src/renderer.tsx b/src/renderer.tsx index 740ad76c6..5d0ad2641 100644 --- a/src/renderer.tsx +++ b/src/renderer.tsx @@ -120,9 +120,6 @@ if (IS_PREVIEW_WINDOW) { observe(rootStore.uiStore, 'windowTitle', ({ object }) => { document.title = object.get(); }); - - // Dim the titlebar when the window is unfocused - } window.addEventListener('beforeunload', () => { @@ -153,7 +150,9 @@ ReactDOM.render( */ async function addTagsToFile(filePath: string, tagNames: string[]) { const { fileStore, tagStore } = rootStore; - const clientFile = fileStore.fileList.find((file) => file.absolutePath === filePath); + const clientFile = runInAction(() => + fileStore.fileList.find((file) => file.absolutePath === filePath), + ); if (clientFile) { const tags = await Promise.all( tagNames.map(async (tagName) => { diff --git a/widgets/Combobox/input.scss b/widgets/Combobox/input.scss index 1ebe5e3fe..30431bbe8 100644 --- a/widgets/Combobox/input.scss +++ b/widgets/Combobox/input.scss @@ -7,6 +7,7 @@ display: flex; flex-grow: 1; flex-wrap: wrap; + overflow-x: hidden; // prevent tags with really long names from extending the width of the input } input { @@ -46,6 +47,11 @@ max-height: 1.75rem; overflow: hidden auto; } + + &.multiline { + height: unset; + max-height: 100vh; + } } /** We cannot use as we want to pick directories. However, diff --git a/widgets/Tree/tree.scss b/widgets/Tree/tree.scss index 87f491559..1ba5fe0e1 100644 --- a/widgets/Tree/tree.scss +++ b/widgets/Tree/tree.scss @@ -38,10 +38,10 @@ transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1); } .spacer { + flex: 1 0 auto; height: 1rem; width: 1rem; margin-right: 0.25rem; - flex-shrink: 0; } } diff --git a/yarn.lock b/yarn.lock index 663803ba1..da03f11ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3303,10 +3303,10 @@ electron-updater@^4.3.8: lodash.isequal "^4.5.0" semver "^7.3.4" -electron@15.0.0: - version "15.0.0" - resolved "https://registry.yarnpkg.com/electron/-/electron-15.0.0.tgz#b1b6244b1cffddf348c27c54b1310b3a3680246e" - integrity sha512-LlBjN5nCJoC7EDrgfDQwEGSGSAo/o08nSP5uJxN2m+ZtNA69SxpnWv4yPgo1K08X/iQPoGhoZu6C8LYYlk1Dtg== +electron@^15.3.3: + version "15.3.3" + resolved "https://registry.yarnpkg.com/electron/-/electron-15.3.3.tgz#e66c6c6fbcd74641dbfafe5e101228d2b7734c7b" + integrity sha512-tr4UaMosN6+s8vSbx6OxqRXDTTCBjjJkmDMv0b0sg8f+cRFQeY0u7xYbULpXS4B1+hHJmdh7Nz40Qpv0bJXa6w== dependencies: "@electron/get" "^1.13.0" "@types/node" "^14.6.2"