UI and filter fixes (#2686)

* Use primitive string in recommendation row props
* Use unique keys in recommendation rows

The keys for the cards used while loading clash with the ids of the actual cards, causing a list unique key warning.

* List filter alignment tweaks
* Rework list hook filtering
* Internationalise checksum correctly
This commit is contained in:
DingDongSoLong4
2022-06-22 07:45:47 +02:00
committed by GitHub
parent 3b4b20e9b2
commit 63e1bbf35d
13 changed files with 171 additions and 150 deletions

View File

@@ -2,7 +2,7 @@ import React, { PropsWithChildren } from "react";
interface IProps {
className?: string;
header: String;
header: string;
link: JSX.Element;
}

View File

@@ -10,7 +10,7 @@ import { FormattedMessage } from "react-intl";
interface IProps {
isTouch: boolean;
filter: ListFilterModel;
header: String;
header: string;
}
export const GalleryRecommendationRow: FunctionComponent<IProps> = (
@@ -41,7 +41,10 @@ export const GalleryRecommendationRow: FunctionComponent<IProps> = (
>
{result.loading
? [...Array(props.filter.itemsPerPage)].map((i) => (
<div key={i} className="gallery-skeleton skeleton-card"></div>
<div
key={`_${i}`}
className="gallery-skeleton skeleton-card"
></div>
))
: result.data?.findGalleries.galleries.map((g) => (
<GalleryCard key={g.id} gallery={g} zoomIndex={1} />

View File

@@ -10,7 +10,7 @@ import { ImageCard } from "./ImageCard";
interface IProps {
isTouch: boolean;
filter: ListFilterModel;
header: String;
header: string;
}
export const ImageRecommendationRow: FunctionComponent<IProps> = (
@@ -41,7 +41,7 @@ export const ImageRecommendationRow: FunctionComponent<IProps> = (
>
{result.loading
? [...Array(props.filter.itemsPerPage)].map((i) => (
<div key={i} className="image-skeleton skeleton-card"></div>
<div key={`_${i}`} className="image-skeleton skeleton-card"></div>
))
: result.data?.findImages.images.map((i) => (
<ImageCard key={i.id} image={i} zoomIndex={1} />

View File

@@ -217,8 +217,8 @@ export const ListFilter: React.FC<IListFilterProps> = ({
return (
<>
<div className="d-flex mb-1">
<div className="mr-2 flex-grow-1 query-text-field-group">
<div className="mb-2 mr-2 d-flex">
<div className="flex-grow-1 query-text-field-group">
<FormControl
ref={queryRef}
placeholder={`${intl.formatMessage({ id: "actions.search" })}…`}
@@ -240,7 +240,7 @@ export const ListFilter: React.FC<IListFilterProps> = ({
</div>
</div>
<ButtonGroup className="mr-2 mb-1">
<ButtonGroup className="mr-2 mb-2">
<Dropdown>
<OverlayTrigger
placement="top"
@@ -277,7 +277,7 @@ export const ListFilter: React.FC<IListFilterProps> = ({
</OverlayTrigger>
</ButtonGroup>
<Dropdown as={ButtonGroup} className="mr-2 mb-1">
<Dropdown as={ButtonGroup} className="mr-2 mb-2">
<InputGroup.Prepend>
<Dropdown.Toggle variant="secondary">
{currentSortBy
@@ -322,13 +322,13 @@ export const ListFilter: React.FC<IListFilterProps> = ({
)}
</Dropdown>
<div>
<div className="mb-2">
<Form.Control
as="select"
ref={perPageSelect}
onChange={(e) => onChangePageSize(e.target.value)}
value={filter.itemsPerPage.toString()}
className="btn-secondary mx-1 mb-1"
className="btn-secondary"
>
{pageSizeOptions.map((s) => (
<option value={s.value} key={s.value}>

View File

@@ -100,7 +100,7 @@ export const ListOperationButtons: React.FC<IListOperationButtonsProps> = ({
if (buttons.length > 0) {
return (
<ButtonGroup className="ml-2 mb-1">
<ButtonGroup className="ml-2 mb-2">
{buttons.map((button) => {
return (
<OverlayTrigger
@@ -176,7 +176,7 @@ export const ListOperationButtons: React.FC<IListOperationButtonsProps> = ({
if (options.length > 0) {
return (
<Dropdown className="mb-1">
<Dropdown>
<Dropdown.Toggle variant="secondary" id="more-menu">
<Icon icon={faEllipsisH} />
</Dropdown.Toggle>
@@ -192,7 +192,7 @@ export const ListOperationButtons: React.FC<IListOperationButtonsProps> = ({
<>
{maybeRenderButtons()}
<div className="mx-2">{renderMore()}</div>
<div className="mx-2 mb-2">{renderMore()}</div>
</>
);
};

View File

@@ -110,7 +110,7 @@ export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
}
return (
<ButtonGroup>
<ButtonGroup className="mb-2">
{displayModeOptions.map((option) => (
<OverlayTrigger
key={option}
@@ -140,9 +140,9 @@ export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
function maybeRenderZoom() {
if (onSetZoom && displayMode === DisplayMode.Grid) {
return (
<div className="align-middle">
<div className="ml-2 mb-2 d-none d-sm-inline-flex">
<Form.Control
className="zoom-slider d-none d-sm-inline-flex ml-3"
className="zoom-slider ml-1"
type="range"
min={minZoom}
max={maxZoom}
@@ -158,7 +158,7 @@ export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
return (
<>
<ButtonGroup>{maybeRenderDisplayModeOptions()}</ButtonGroup>
{maybeRenderDisplayModeOptions()}
{maybeRenderZoom()}
</>
);

View File

@@ -10,7 +10,7 @@ import { FormattedMessage } from "react-intl";
interface IProps {
isTouch: boolean;
filter: ListFilterModel;
header: String;
header: string;
}
export const MovieRecommendationRow: React.FC<IProps> = (props: IProps) => {
@@ -39,7 +39,7 @@ export const MovieRecommendationRow: React.FC<IProps> = (props: IProps) => {
>
{result.loading
? [...Array(props.filter.itemsPerPage)].map((i) => (
<div key={i} className="movie-skeleton skeleton-card"></div>
<div key={`_${i}`} className="movie-skeleton skeleton-card"></div>
))
: result.data?.findMovies.movies.map((m) => (
<MovieCard key={m.id} movie={m} />

View File

@@ -10,7 +10,7 @@ import { FormattedMessage } from "react-intl";
interface IProps {
isTouch: boolean;
filter: ListFilterModel;
header: String;
header: string;
}
export const PerformerRecommendationRow: FunctionComponent<IProps> = (
@@ -41,7 +41,10 @@ export const PerformerRecommendationRow: FunctionComponent<IProps> = (
>
{result.loading
? [...Array(props.filter.itemsPerPage)].map((i) => (
<div key={i} className="performer-skeleton skeleton-card"></div>
<div
key={`_${i}`}
className="performer-skeleton skeleton-card"
></div>
))
: result.data?.findPerformers.performers.map((p) => (
<PerformerCard key={p.id} performer={p} />

View File

@@ -11,7 +11,7 @@ import { FormattedMessage } from "react-intl";
interface IProps {
isTouch: boolean;
filter: ListFilterModel;
header: String;
header: string;
}
export const SceneRecommendationRow: FunctionComponent<IProps> = (
@@ -46,7 +46,7 @@ export const SceneRecommendationRow: FunctionComponent<IProps> = (
>
{result.loading
? [...Array(props.filter.itemsPerPage)].map((i) => (
<div key={i} className="scene-skeleton skeleton-card"></div>
<div key={`_${i}`} className="scene-skeleton skeleton-card"></div>
))
: result.data?.findScenes.scenes.map((scene, index) => (
<SceneCard

View File

@@ -10,7 +10,7 @@ import { FormattedMessage } from "react-intl";
interface IProps {
isTouch: boolean;
filter: ListFilterModel;
header: String;
header: string;
}
export const StudioRecommendationRow: FunctionComponent<IProps> = (
@@ -41,7 +41,10 @@ export const StudioRecommendationRow: FunctionComponent<IProps> = (
>
{result.loading
? [...Array(props.filter.itemsPerPage)].map((i) => (
<div key={i} className="studio-skeleton skeleton-card"></div>
<div
key={`_${i}`}
className="studio-skeleton skeleton-card"
></div>
))
: result.data?.findStudios.studios.map((s) => (
<StudioCard key={s.id} studio={s} hideParent={true} />

View File

@@ -10,7 +10,7 @@ import { FormattedMessage } from "react-intl";
interface IProps {
isTouch: boolean;
filter: ListFilterModel;
header: String;
header: string;
}
export const TagRecommendationRow: FunctionComponent<IProps> = (
@@ -41,7 +41,7 @@ export const TagRecommendationRow: FunctionComponent<IProps> = (
>
{result.loading
? [...Array(props.filter.itemsPerPage)].map((i) => (
<div key={i} className="tag-skeleton skeleton-card"></div>
<div key={`_${i}`} className="tag-skeleton skeleton-card"></div>
))
: result.data?.findTags.tags.map((p) => (
<TagCard key={p.id} tag={p} zoomIndex={0} />

View File

@@ -1,5 +1,6 @@
import clone from "lodash-es/clone";
import cloneDeep from "lodash-es/cloneDeep";
import isEqual from "lodash-es/isEqual";
import queryString from "query-string";
import React, {
useCallback,
@@ -169,7 +170,7 @@ interface IRenderListProps {
filter: ListFilterModel;
filterOptions: ListFilterOptions;
onChangePage: (page: number) => void;
updateQueryParams: (filter: ListFilterModel) => void;
updateFilter: (filter: ListFilterModel) => void;
}
const RenderList = <
@@ -190,7 +191,7 @@ const RenderList = <
selectable,
renderEditDialog,
renderDeleteDialog,
updateQueryParams,
updateFilter,
filterDialog,
persistState,
}: IListHookOptions<QueryResult, QueryData> &
@@ -342,11 +343,11 @@ const RenderList = <
function onChangeZoom(newZoomIndex: number) {
const newFilter = cloneDeep(filter);
newFilter.zoomIndex = newZoomIndex;
updateQueryParams(newFilter);
updateFilter(newFilter);
}
async function onOperationClicked(o: IListHookOperation<QueryResult>) {
await o.onClick(result, filter, selectedIds);
function onOperationClicked(o: IListHookOperation<QueryResult>) {
o.onClick(result, filter, selectedIds);
if (o.postRefetch) {
result.refetch();
}
@@ -437,7 +438,7 @@ const RenderList = <
function onChangeDisplayMode(displayMode: DisplayMode) {
const newFilter = cloneDeep(filter);
newFilter.displayMode = displayMode;
updateQueryParams(newFilter);
updateFilter(newFilter);
}
function onAddCriterion(
@@ -464,7 +465,7 @@ const RenderList = <
});
newFilter.currentPage = 1;
updateQueryParams(newFilter);
updateFilter(newFilter);
setEditingCriterion(undefined);
setNewCriterion(false);
}
@@ -475,7 +476,7 @@ const RenderList = <
(criterion) => criterion.getId() !== removedCriterion.getId()
);
newFilter.currentPage = 1;
updateQueryParams(newFilter);
updateFilter(newFilter);
}
function updateCriteria(c: Criterion<CriterionValue>[]) {
@@ -491,9 +492,9 @@ const RenderList = <
const content = (
<div>
<ButtonToolbar className="align-items-center justify-content-center mb-2">
<ButtonToolbar className="justify-content-center">
<ListFilter
onFilterUpdate={updateQueryParams}
onFilterUpdate={updateFilter}
filter={filter}
filterOptions={filterOptions}
openFilterDialog={() => setNewCriterion(true)}
@@ -563,29 +564,32 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
const history = useHistory();
const location = useLocation();
const [interfaceState, setInterfaceState] = useInterfaceLocalForage();
// If persistState is false we don't care about forage and consider it initialised
const [forageInitialised, setForageInitialised] = useState(
!options.persistState
);
const [filterInitialised, setFilterInitialised] = useState(false);
// Store initial pathname to prevent hooks from operating outside this page
const originalPathName = useRef(location.pathname);
const persistanceKey = options.persistanceKey ?? options.filterMode;
const defaultSort = options.defaultSort ?? filterOptions.defaultSortBy;
const defaultDisplayMode = filterOptions.displayModeOptions[0];
const [filter, setFilter] = useState<ListFilterModel>(
new ListFilterModel(
const createNewFilter = useCallback(() => {
return new ListFilterModel(
options.filterMode,
queryString.parse(location.search),
queryString.parse(history.location.search),
defaultSort,
defaultDisplayMode,
options.defaultZoomIndex
)
);
}, [
options.filterMode,
history,
defaultSort,
defaultDisplayMode,
options.defaultZoomIndex,
]);
const [filter, setFilter] = useState<ListFilterModel>(createNewFilter);
const updateInterfaceConfig = useCallback(
(updatedFilter: ListFilterModel, level: PersistanceLevel) => {
if (level === PersistanceLevel.VIEW) {
const updateSavedFilter = useCallback(
(updatedFilter: ListFilterModel) => {
setInterfaceState((prevState) => {
if (!prevState.queryConfig) {
prevState.queryConfig = {};
@@ -606,7 +610,6 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
},
};
});
}
},
[persistanceKey, setInterfaceState]
);
@@ -617,54 +620,38 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
} = useFindDefaultFilter(options.filterMode);
const updateQueryParams = useCallback(
(listFilter: ListFilterModel) => {
setFilter(listFilter);
const newLocation = { ...location };
newLocation.search = listFilter.makeQueryParameters();
history.replace(newLocation);
if (options.persistState) {
updateInterfaceConfig(listFilter, options.persistState);
(newFilter: ListFilterModel) => {
const newParams = newFilter.makeQueryParameters();
history.replace({ ...history.location, search: newParams });
},
[history]
);
const updateFilter = useCallback(
(newFilter: ListFilterModel) => {
setFilter(newFilter);
updateQueryParams(newFilter);
if (options.persistState === PersistanceLevel.VIEW) {
updateSavedFilter(newFilter);
}
},
[setFilter, history, location, options.persistState, updateInterfaceConfig]
[options.persistState, updateSavedFilter, updateQueryParams]
);
// 'Startup' hook, initialises the filters
useEffect(() => {
if (
// defer processing this until forage is initialised and
// default filter is loaded
interfaceState.loading ||
defaultFilterLoading ||
// Only update query params on page the hook was mounted on
history.location.pathname !== originalPathName.current
)
return;
// Only run once
if (filterInitialised) return;
if (!forageInitialised) setForageInitialised(true);
let newFilter = filter.clone();
const newFilter = filter.clone();
let update = false;
if (options.persistState === PersistanceLevel.ALL) {
// only set default filter if query params are empty
if (!history.location.search) {
// wait until default filter is loaded
if (defaultFilterLoading) return;
// Compare constructed filter with current filter.
// If different it's the result of navigation, and we update the filter.
if (
history.location.search &&
history.location.search !== `?${filter.makeQueryParameters()}`
) {
newFilter.configureFromQueryParameters(
queryString.parse(history.location.search)
);
update = true;
}
// if default query is set and no search params are set, then
// load the default query
// #1512 - use default query only if persistState is ALL
if (
options.persistState === PersistanceLevel.ALL &&
!location.search &&
defaultFilter?.findDefaultFilter
) {
if (defaultFilter?.findDefaultFilter) {
newFilter.currentPage = 1;
try {
newFilter.configureFromQueryParameters(
@@ -676,50 +663,75 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
}
// #1507 - reset random seed when loaded
newFilter.randomSeed = -1;
update = true;
}
}
} else if (options.persistState === PersistanceLevel.VIEW) {
// wait until forage is initialised
if (interfaceState.loading) return;
// set the display type if persisted
const storedQuery = interfaceState.data?.queryConfig?.[persistanceKey];
if (options.persistState === PersistanceLevel.VIEW && storedQuery) {
const storedFilter = queryString.parse(storedQuery.filter);
if (storedFilter.disp !== undefined) {
const displayMode = Number.parseInt(storedFilter.disp as string, 10);
if (displayMode !== newFilter.displayMode) {
newFilter.displayMode = displayMode;
update = true;
}
}
}
if (update) {
setFilter(newFilter);
updateQueryParams(newFilter);
}
setFilterInitialised(true);
}, [
defaultSort,
defaultDisplayMode,
filterInitialised,
filter,
interfaceState,
history,
location.search,
options.persistState,
updateQueryParams,
defaultFilter,
defaultFilterLoading,
interfaceState,
persistanceKey,
forageInitialised,
options.persistState,
]);
// This hook runs on every page location change (ie navigation),
// and updates the filter accordingly.
useEffect(() => {
if (!filterInitialised) return;
// Only update on page the hook was mounted on
if (location.pathname !== originalPathName.current) {
return;
}
// Re-init filters on empty new query params
if (!location.search) {
setFilter(createNewFilter);
setFilterInitialised(false);
return;
}
setFilter((prevFilter) => {
let newFilter = prevFilter.clone();
newFilter.configureFromQueryParameters(
queryString.parse(location.search)
);
if (!isEqual(newFilter, prevFilter)) {
return newFilter;
} else {
return prevFilter;
}
});
}, [filterInitialised, createNewFilter, location]);
const onChangePage = useCallback(
(page: number) => {
const newFilter = cloneDeep(filter);
newFilter.currentPage = page;
updateQueryParams(newFilter);
updateFilter(newFilter);
window.scrollTo(0, 0);
},
[filter, updateQueryParams]
[filter, updateFilter]
);
const renderFilter = useMemo(() => {
@@ -731,10 +743,10 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
filter: renderFilter,
filterOptions,
onChangePage,
updateQueryParams,
updateFilter,
});
const template = !forageInitialised ? (
const template = !filterInitialised ? (
<LoadingIndicator />
) : (
<>{contentTemplate}</>

View File

@@ -53,7 +53,7 @@ export function makeCriteria(type: CriterionType = "none") {
case "path":
case "checksum":
return new StringCriterion(
new MandatoryStringCriterionOption(type, type)
new MandatoryStringCriterionOption("media_info.checksum", type, type)
);
case "oshash":
return new StringCriterion(
@@ -134,7 +134,7 @@ export function makeCriteria(type: CriterionType = "none") {
case "sceneChecksum":
case "galleryChecksum":
return new StringCriterion(
new StringCriterionOption("checksum", type, "checksum")
new StringCriterionOption("media_info.checksum", type, "checksum")
);
case "phash":
return new StringCriterion(PhashCriterionOption);