Add filtering to folder select (#4277)

This commit is contained in:
InfiniteStash 2023-11-20 03:00:28 +01:00 committed by GitHub
parent 61f4d8bd12
commit a0f33e3dab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 120 additions and 85 deletions

View file

@ -4,6 +4,7 @@ import (
"io/fs"
"os"
"path/filepath"
"strings"
"golang.org/x/text/collate"
)
@ -25,13 +26,27 @@ func (s dirLister) Bytes(i int) []byte {
// listDir will return the contents of a given directory path as a string slice
func listDir(col *collate.Collator, path string) ([]string, error) {
var dirPaths []string
dirPath := path
files, err := os.ReadDir(path)
if err != nil {
path = filepath.Dir(path)
files, err = os.ReadDir(path)
dirPath = filepath.Dir(path)
dirFiles, err := os.ReadDir(dirPath)
if err != nil {
return dirPaths, err
}
// Filter dir contents by last path fragment if the dir isn't an exact match
base := strings.ToLower(filepath.Base(path))
if base != "." && base != string(filepath.Separator) {
for _, file := range dirFiles {
if strings.HasPrefix(strings.ToLower(file.Name()), base) {
files = append(files, file)
}
}
} else {
files = dirFiles
}
}
if col != nil {
@ -42,7 +57,7 @@ func listDir(col *collate.Collator, path string) ([]string, error) {
if !file.IsDir() {
continue
}
dirPaths = append(dirPaths, filepath.Join(path, file.Name()))
dirPaths = append(dirPaths, filepath.Join(dirPath, file.Name()))
}
return dirPaths, nil
}

View file

@ -50,7 +50,7 @@ func getDir(path string) string {
}
func getParent(path string) *string {
isRoot := path[len(path)-1:] == "/"
isRoot := path == "/"
if isRoot {
return nil
} else {

View file

@ -37,9 +37,9 @@ export const PathFilter: React.FC<IInputFilterProps> = ({
) : (
<FolderSelect
currentDirectory={criterion.value ? criterion.value.toString() : ""}
setCurrentDirectory={(v) => onValueChanged(v)}
onChangeDirectory={onValueChanged}
collapsible
quoteSpaced
quotePath
hideError
defaultDirectories={libraryPaths}
/>

View file

@ -106,7 +106,7 @@ const CleanDialog: React.FC<ICleanDialog> = ({
{pathSelection ? (
<FolderSelect
currentDirectory={currentDirectory}
setCurrentDirectory={(v) => setCurrentDirectory(v)}
onChangeDirectory={setCurrentDirectory}
defaultDirectories={libraryPaths}
appendButton={
<Button

View file

@ -80,7 +80,7 @@ export const DirectorySelectionDialog: React.FC<
<FolderSelect
currentDirectory={currentDirectory}
setCurrentDirectory={(v) => setCurrentDirectory(v)}
onChangeDirectory={setCurrentDirectory}
defaultDirectories={libraryPaths}
appendButton={
<Button

View file

@ -1,89 +1,76 @@
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { Button, InputGroup, Form, Collapse } from "react-bootstrap";
import { Icon } from "../Icon";
import { LoadingIndicator } from "../LoadingIndicator";
import { useDirectory } from "src/core/StashService";
import { faEllipsis, faTimes } from "@fortawesome/free-solid-svg-icons";
import { useDebounce } from "src/hooks/debounce";
import TextUtils from "src/utils/text";
import { useDirectoryPaths } from "./useDirectoryPaths";
interface IProps {
currentDirectory: string;
setCurrentDirectory: (value: string) => void;
onChangeDirectory: (value: string) => void;
defaultDirectories?: string[];
appendButton?: JSX.Element;
collapsible?: boolean;
quoteSpaced?: boolean;
quotePath?: boolean;
hideError?: boolean;
}
export const FolderSelect: React.FC<IProps> = ({
currentDirectory,
setCurrentDirectory,
defaultDirectories,
onChangeDirectory,
defaultDirectories = [],
appendButton,
collapsible = false,
quoteSpaced = false,
quotePath = false,
hideError = false,
}) => {
const [showBrowser, setShowBrowser] = React.useState(false);
const [directory, setDirectory] = useState(currentDirectory);
const intl = useIntl();
const [showBrowser, setShowBrowser] = useState(false);
const [path, setPath] = useState(currentDirectory);
const isQuoted =
quoteSpaced && directory.startsWith('"') && directory.endsWith('"');
const { data, error, loading } = useDirectory(
isQuoted ? directory.slice(1, -1) : directory
const normalizedPath = quotePath ? TextUtils.stripQuotes(path) : path;
const { directories, parent, error, loading } = useDirectoryPaths(
normalizedPath,
hideError
);
const intl = useIntl();
const selectableDirectories =
(currentDirectory ? directories : defaultDirectories) ?? defaultDirectories;
const defaultDirectoriesOrEmpty = defaultDirectories ?? [];
const selectableDirectories: string[] = currentDirectory
? data?.directory.directories ??
(error && hideError ? [] : defaultDirectoriesOrEmpty)
: defaultDirectoriesOrEmpty;
const debouncedSetDirectory = useDebounce(setDirectory, 250);
useEffect(() => {
if (currentDirectory !== directory) {
debouncedSetDirectory(currentDirectory);
}
}, [currentDirectory, directory, debouncedSetDirectory]);
const debouncedSetDirectory = useDebounce(setPath, 250);
function setInstant(value: string) {
if (quoteSpaced && value.includes(" ")) {
value = `"${value}"`;
}
setCurrentDirectory(value);
setDirectory(value);
const normalizedValue =
quotePath && value.includes(" ") ? TextUtils.addQuotes(value) : value;
onChangeDirectory(normalizedValue);
setPath(normalizedValue);
}
function setDebounced(value: string) {
setCurrentDirectory(value);
onChangeDirectory(value);
debouncedSetDirectory(value);
}
function goUp() {
if (defaultDirectories?.includes(currentDirectory)) {
setInstant("");
} else if (data?.directory.parent) {
setInstant(data.directory.parent);
} else if (parent) {
setInstant(parent);
}
}
const topDirectory =
currentDirectory && data?.directory?.parent ? (
<li className="folder-list-parent folder-list-item">
<Button variant="link" onClick={() => goUp()}>
<span>
<FormattedMessage id="setup.folder.up_dir" />
</span>
</Button>
</li>
) : null;
const topDirectory = currentDirectory && parent && (
<li className="folder-list-parent folder-list-item">
<Button variant="link" onClick={() => goUp()} disabled={loading}>
<span>
<FormattedMessage id="setup.folder.up_dir" />
</span>
</Button>
</li>
);
return (
<>
@ -91,16 +78,16 @@ export const FolderSelect: React.FC<IProps> = ({
<Form.Control
className="btn-secondary"
placeholder={intl.formatMessage({ id: "setup.folder.file_path" })}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
onChange={(e) => {
setDebounced(e.currentTarget.value);
}}
value={currentDirectory}
spellCheck={false}
/>
{appendButton ? (
<InputGroup.Append>{appendButton}</InputGroup.Append>
) : undefined}
{collapsible ? (
{appendButton && <InputGroup.Append>{appendButton}</InputGroup.Append>}
{collapsible && (
<InputGroup.Append>
<Button
variant="secondary"
@ -109,32 +96,37 @@ export const FolderSelect: React.FC<IProps> = ({
<Icon icon={faEllipsis} />
</Button>
</InputGroup.Append>
) : undefined}
{!data || !data.directory || loading ? (
)}
{(loading || error) && (
<InputGroup.Append className="align-self-center">
{loading ? (
<LoadingIndicator inline small message="" />
) : !hideError ? (
<Icon icon={faTimes} color="red" className="ml-3" />
) : undefined}
) : (
!hideError && <Icon icon={faTimes} color="red" className="ml-3" />
)}
</InputGroup.Append>
) : undefined}
)}
</InputGroup>
{!hideError && error !== undefined && (
<h5 className="mt-4 text-break">Error: {error.message}</h5>
)}
<Collapse in={!collapsible || showBrowser}>
<ul className="folder-list">
{topDirectory}
{selectableDirectories.map((path) => {
return (
<li key={path} className="folder-list-item">
<Button variant="link" onClick={() => setInstant(path)}>
<span>{path}</span>
</Button>
</li>
);
})}
{selectableDirectories.map((dir) => (
<li key={dir} className="folder-list-item">
<Button
variant="link"
onClick={() => setInstant(dir)}
disabled={loading}
>
<span>{dir}</span>
</Button>
</li>
))}
</ul>
</Collapse>
</>

View file

@ -23,7 +23,7 @@ export const FolderSelectDialog: React.FC<IProps> = ({
<div className="dialog-content">
<FolderSelect
currentDirectory={currentDirectory}
setCurrentDirectory={(v) => setCurrentDirectory(v)}
onChangeDirectory={setCurrentDirectory}
/>
</div>
</Modal.Body>

View file

@ -0,0 +1,16 @@
import { useRef } from "react";
import { useDirectory } from "src/core/StashService";
export const useDirectoryPaths = (path: string, hideError: boolean) => {
const { data, loading, error } = useDirectory(path);
const prevData = useRef<typeof data | undefined>(undefined);
if (!loading) prevData.current = data;
const currentData = loading ? prevData.current : data;
const directories =
error && hideError ? [] : currentData?.directory.directories;
const parent = currentData?.directory.parent;
return { directories, parent, loading, error };
};

View file

@ -80,10 +80,9 @@
}
}
.folder-list {
margin-top: 0.5rem 0 0 0;
max-height: 30vw;
overflow-x: auto;
// z-index gets set on button groups for some reason
.multi-set .btn-group > button.btn {
z-index: auto;
}
.folder-item {
@ -92,14 +91,12 @@
}
}
// z-index gets set on button groups for some reason
.multi-set .btn-group > button.btn {
z-index: auto;
}
.folder-list {
list-style-type: none;
margin: 0;
max-height: 30vw;
overflow-x: auto;
padding-bottom: 0.5rem;
padding-top: 1rem;
&-item {

View file

@ -455,6 +455,19 @@ const abbreviateCounter = (counter: number = 0) => {
};
};
/*
* Trims quotes if the text has leading/trailing quotes
*/
const stripQuotes = (text: string) => {
if (text.startsWith('"') && text.endsWith('"')) return text.slice(1, -1);
return text;
};
/*
* Wraps string in quotes
*/
const addQuotes = (text: string) => `"${text}"`;
const TextUtils = {
fileSize,
formatFileSizeUnit,
@ -478,6 +491,8 @@ const TextUtils = {
formatDateTime,
secondsAsTimeString,
abbreviateCounter,
stripQuotes,
addQuotes,
};
export default TextUtils;