mirror of
https://github.com/stashapp/stash.git
synced 2025-12-10 10:22:18 +01:00
Fix various console errors and graphql loading issues (#760)
* Refactor listhook to resolve loading issues * Fix graphql loading race conditions * Various console spam * Fix scene card overlay hierarchy * Fix modal and manual borders
This commit is contained in:
parent
9a84726128
commit
fef16d7e09
16 changed files with 345 additions and 325 deletions
|
|
@ -132,7 +132,7 @@ export const Manual: React.FC<IManualProps> = ({ show, onClose }) => {
|
|||
<Nav variant="pills" className="flex-column">
|
||||
{content.map((c) => {
|
||||
return (
|
||||
<Nav.Item>
|
||||
<Nav.Item key={`${c.key}-nav`}>
|
||||
<Nav.Link className={c.className} eventKey={c.key}>
|
||||
{c.title}
|
||||
</Nav.Link>
|
||||
|
|
@ -146,7 +146,11 @@ export const Manual: React.FC<IManualProps> = ({ show, onClose }) => {
|
|||
<Tab.Content>
|
||||
{content.map((c) => {
|
||||
return (
|
||||
<Tab.Pane eventKey={c.key} onClick={interceptLinkClick}>
|
||||
<Tab.Pane
|
||||
eventKey={c.key}
|
||||
key={`${c.key}-pane`}
|
||||
onClick={interceptLinkClick}
|
||||
>
|
||||
<Page page={c.content} />
|
||||
</Tab.Pane>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
.manual {
|
||||
background-color: #30404d;
|
||||
color: $text-color;
|
||||
|
||||
.close {
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
.manual-container {
|
||||
&-container {
|
||||
padding-left: 1px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.modal-header,
|
||||
.modal-body {
|
||||
&-header,
|
||||
&-body {
|
||||
background-color: #30404d;
|
||||
color: $text-color;
|
||||
overflow-y: hidden;
|
||||
|
|
@ -21,22 +20,22 @@
|
|||
.indent-1 {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.manual .manual-content,
|
||||
.manual .manual-toc {
|
||||
max-height: calc(100vh - 10rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.manual .modal-body {
|
||||
.manual-content,
|
||||
.manual-toc {
|
||||
max-height: calc(100vh - 10rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.manual-content,
|
||||
.manual-toc {
|
||||
max-height: inherit;
|
||||
overflow-y: hidden;
|
||||
@media (max-width: 992px) {
|
||||
.modal-body {
|
||||
overflow-y: auto;
|
||||
|
||||
.manual-content,
|
||||
.manual-toc {
|
||||
max-height: inherit;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -285,6 +285,10 @@ export const Performer: React.FC = () => {
|
|||
|
||||
const photos = [{ src: activeImage, caption: "Image" }];
|
||||
|
||||
if (!performer.id) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="performer-page" className="row">
|
||||
<div className="image-container col-md-4 text-center">
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
|||
if (props.scene.performers.length <= 0) return;
|
||||
|
||||
const popoverContent = props.scene.performers.map((performer) => (
|
||||
<div className="performer-tag-container row" key="performer">
|
||||
<div className="performer-tag-container row" key={performer.id}>
|
||||
<Link
|
||||
to={`/performers/${performer.id}`}
|
||||
className="performer-tag col m-auto zoom-2"
|
||||
|
|
@ -151,7 +151,11 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
|||
));
|
||||
|
||||
return (
|
||||
<HoverPopover placement="bottom" content={popoverContent}>
|
||||
<HoverPopover
|
||||
placement="bottom"
|
||||
content={popoverContent}
|
||||
className="tag-tooltip"
|
||||
>
|
||||
<Button className="minimal">
|
||||
<Icon icon="film" />
|
||||
<span>{props.scene.movies.length}</span>
|
||||
|
|
@ -285,26 +289,28 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
|||
}}
|
||||
/>
|
||||
|
||||
<Link
|
||||
to={`/scenes/${props.scene.id}`}
|
||||
className="scene-card-link"
|
||||
onClick={handleSceneClick}
|
||||
onDragStart={handleDrag}
|
||||
onDragOver={handleDragOver}
|
||||
draggable={props.selecting}
|
||||
>
|
||||
{maybeRenderRatingBanner()}
|
||||
{maybeRenderSceneStudioOverlay()}
|
||||
{maybeRenderSceneSpecsOverlay()}
|
||||
<video
|
||||
loop
|
||||
className={cx("scene-card-video", { portrait: isPortrait() })}
|
||||
poster={props.scene.paths.screenshot || ""}
|
||||
ref={hoverHandler.videoEl}
|
||||
<div className="video-section">
|
||||
<Link
|
||||
to={`/scenes/${props.scene.id}`}
|
||||
className="scene-card-link"
|
||||
onClick={handleSceneClick}
|
||||
onDragStart={handleDrag}
|
||||
onDragOver={handleDragOver}
|
||||
draggable={props.selecting}
|
||||
>
|
||||
{previewPath ? <source src={previewPath} /> : ""}
|
||||
</video>
|
||||
</Link>
|
||||
{maybeRenderRatingBanner()}
|
||||
{maybeRenderSceneSpecsOverlay()}
|
||||
<video
|
||||
loop
|
||||
className={cx("scene-card-video", { portrait: isPortrait() })}
|
||||
poster={props.scene.paths.screenshot || ""}
|
||||
ref={hoverHandler.videoEl}
|
||||
>
|
||||
{previewPath ? <source src={previewPath} /> : ""}
|
||||
</video>
|
||||
</Link>
|
||||
{maybeRenderSceneStudioOverlay()}
|
||||
</div>
|
||||
<div className="card-section">
|
||||
<h5 className="card-section-title">
|
||||
{props.scene.title
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ export const RatingStars: React.FC<IRatingStarsProps> = (
|
|||
onFocus={() => onMouseOver(rating)}
|
||||
onBlur={() => onMouseOut(rating)}
|
||||
title={getTooltip(rating)}
|
||||
key={`star-${rating}`}
|
||||
>
|
||||
<Icon
|
||||
icon={[getIconPrefix(rating), "star"]}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.video-section {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-section {
|
||||
margin-bottom: 0;
|
||||
padding: 0.5rem 1rem 0 1rem;
|
||||
|
|
@ -174,10 +178,6 @@ textarea.scene-description {
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
&-link {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.scene-card-check {
|
||||
left: 0.5rem;
|
||||
margin-top: -12px;
|
||||
|
|
|
|||
|
|
@ -455,8 +455,10 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||
className="col col-sm-6 text-input"
|
||||
type="number"
|
||||
value={previewSegments.toString()}
|
||||
onInput={(e: React.FormEvent<HTMLInputElement>) =>
|
||||
setPreviewSegments(Number.parseInt(e.currentTarget.value, 10))
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setPreviewSegments(
|
||||
Number.parseInt(e.currentTarget.value || "0", 10)
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
|
|
@ -470,9 +472,9 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||
className="col col-sm-6 text-input"
|
||||
type="number"
|
||||
value={previewSegmentDuration.toString()}
|
||||
onInput={(e: React.FormEvent<HTMLInputElement>) =>
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setPreviewSegmentDuration(
|
||||
Number.parseFloat(e.currentTarget.value)
|
||||
Number.parseFloat(e.currentTarget.value || "0")
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
|
@ -583,8 +585,10 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||
className="col col-sm-6 text-input"
|
||||
type="number"
|
||||
value={maxSessionAge.toString()}
|
||||
onInput={(e: React.FormEvent<HTMLInputElement>) =>
|
||||
setMaxSessionAge(Number.parseInt(e.currentTarget.value, 10))
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setMaxSessionAge(
|
||||
Number.parseInt(e.currentTarget.value || "0", 10)
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ export const HoverPopover: React.FC<IHoverPopover> = ({
|
|||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
id="popover"
|
||||
className="hover-popover-content"
|
||||
>
|
||||
{content}
|
||||
</Popover>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Badge } from "react-bootstrap";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import cx from "classnames";
|
||||
import {
|
||||
PerformerDataFragment,
|
||||
SceneMarkerDataFragment,
|
||||
|
|
@ -43,7 +44,7 @@ export const TagLink: React.FC<IProps> = (props: IProps) => {
|
|||
: TextUtils.fileNameFromPath(props.scene.path ?? "");
|
||||
}
|
||||
return (
|
||||
<Badge className={`tag-item ${props.className}`} variant="secondary">
|
||||
<Badge className={cx("tag-item", props.className)} variant="secondary">
|
||||
<Link to={link}>{title}</Link>
|
||||
</Badge>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -128,3 +128,8 @@ button.collapse-button.btn-primary:not(:disabled):not(.disabled):active {
|
|||
box-shadow: none;
|
||||
color: #f5f8fa;
|
||||
}
|
||||
|
||||
.hover-popover-content {
|
||||
max-width: 32rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ export const Studio: React.FC = () => {
|
|||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, isEditing);
|
||||
|
||||
if (!isNew && !isEditing) {
|
||||
if (!data?.findStudio || loading) return <LoadingIndicator />;
|
||||
if (!data?.findStudio || loading || !studio.id) return <LoadingIndicator />;
|
||||
if (error) return <div>{error.message}</div>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export const Tag: React.FC = () => {
|
|||
const [name, setName] = useState<string>();
|
||||
|
||||
// Tag state
|
||||
const [tag, setTag] = useState<Partial<GQL.TagDataFragment>>({});
|
||||
const [tag, setTag] = useState<GQL.TagDataFragment | undefined>();
|
||||
const [imagePreview, setImagePreview] = useState<string>();
|
||||
|
||||
const { data, error, loading } = useFindTag(id);
|
||||
|
|
@ -71,11 +71,11 @@ export const Tag: React.FC = () => {
|
|||
};
|
||||
});
|
||||
|
||||
function updateTagEditState(state: Partial<GQL.TagDataFragment>) {
|
||||
function updateTagEditState(state: GQL.TagDataFragment) {
|
||||
setName(state.name);
|
||||
}
|
||||
|
||||
function updateTagData(tagData: Partial<GQL.TagDataFragment>) {
|
||||
function updateTagData(tagData: GQL.TagDataFragment) {
|
||||
setImage(undefined);
|
||||
updateTagEditState(tagData);
|
||||
setImagePreview(tagData.image_path ?? undefined);
|
||||
|
|
@ -104,15 +104,17 @@ export const Tag: React.FC = () => {
|
|||
}
|
||||
|
||||
function getTagInput() {
|
||||
const input: Partial<GQL.TagCreateInput | GQL.TagUpdateInput> = {
|
||||
if (!isNew) {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
image,
|
||||
};
|
||||
}
|
||||
return {
|
||||
name,
|
||||
image,
|
||||
};
|
||||
|
||||
if (!isNew) {
|
||||
(input as GQL.TagUpdateInput).id = id;
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
|
|
@ -136,7 +138,7 @@ export const Tag: React.FC = () => {
|
|||
}
|
||||
|
||||
async function onAutoTag() {
|
||||
if (!tag.id) return;
|
||||
if (!tag?.id) return;
|
||||
try {
|
||||
await mutateMetadataAutoTag({ tags: [tag.id] });
|
||||
Toast.success({ content: "Started auto tagging" });
|
||||
|
|
@ -175,13 +177,15 @@ export const Tag: React.FC = () => {
|
|||
|
||||
function onToggleEdit() {
|
||||
setIsEditing(!isEditing);
|
||||
updateTagData(tag);
|
||||
if (tag) {
|
||||
updateTagData(tag);
|
||||
}
|
||||
}
|
||||
|
||||
function onClearImage() {
|
||||
setImage(null);
|
||||
setImagePreview(
|
||||
tag.image_path ? `${tag.image_path}?default=true` : undefined
|
||||
tag?.image_path ? `${tag.image_path}?default=true` : undefined
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -226,7 +230,7 @@ export const Tag: React.FC = () => {
|
|||
acceptSVG
|
||||
/>
|
||||
</div>
|
||||
{!isNew && (
|
||||
{!isNew && tag && (
|
||||
<div className="col col-md-8">
|
||||
<Tabs
|
||||
id="tag-tabs"
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ import { SceneList } from "src/components/Scenes/SceneList";
|
|||
import { TagsCriterion } from "src/models/list-filter/criteria/tags";
|
||||
|
||||
interface ITagScenesPanel {
|
||||
tag: Partial<GQL.TagDataFragment>;
|
||||
tag: GQL.TagDataFragment;
|
||||
}
|
||||
|
||||
export const TagScenesPanel: React.FC<ITagScenesPanel> = ({ tag }) => {
|
||||
function filterHook(filter: ListFilterModel) {
|
||||
const tagValue = { id: tag.id!, label: tag.name! };
|
||||
const tagValue = { id: tag.id, label: tag.name };
|
||||
// if tag is already present, then we modify it, otherwise add
|
||||
let tagCriterion = filter.criteria.find((c) => {
|
||||
return c.type === "tags";
|
||||
|
|
|
|||
|
|
@ -38,6 +38,24 @@ import {
|
|||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { FilterMode } from "src/models/list-filter/types";
|
||||
|
||||
const getSelectedData = <I extends IDataItem>(
|
||||
result: I[],
|
||||
selectedIds: Set<string>
|
||||
) => {
|
||||
// find the selected items from the ids
|
||||
const selectedResults: I[] = [];
|
||||
|
||||
selectedIds.forEach((id) => {
|
||||
const item = result.find((s) => s.id === id);
|
||||
|
||||
if (item) {
|
||||
selectedResults.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return selectedResults;
|
||||
};
|
||||
|
||||
interface IListHookData {
|
||||
filter: ListFilterModel;
|
||||
template: JSX.Element;
|
||||
|
|
@ -99,34 +117,45 @@ interface IQuery<T extends IQueryResult, T2 extends IDataItem> {
|
|||
filterMode: FilterMode;
|
||||
useData: (filter: ListFilterModel) => T;
|
||||
getData: (data: T) => T2[];
|
||||
getSelectedData: (data: T, selectedIds: Set<string>) => T2[];
|
||||
getCount: (data: T) => number;
|
||||
}
|
||||
|
||||
const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
||||
options: IListHookOptions<QueryResult, QueryData> &
|
||||
IQuery<QueryResult, QueryData>
|
||||
): IListHookData => {
|
||||
const [interfaceState, setInterfaceState] = useInterfaceLocalForage();
|
||||
const [forageInitialised, setForageInitialised] = useState(false);
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const [filter, setFilter] = useState<ListFilterModel>(
|
||||
new ListFilterModel(options.filterMode, queryString.parse(location.search))
|
||||
);
|
||||
interface IRenderListProps {
|
||||
filter: ListFilterModel;
|
||||
onChangePage: (page: number) => void;
|
||||
updateQueryParams: (filter: ListFilterModel) => void;
|
||||
}
|
||||
|
||||
const RenderList = <
|
||||
QueryResult extends IQueryResult,
|
||||
QueryData extends IDataItem
|
||||
>({
|
||||
defaultZoomIndex,
|
||||
filter,
|
||||
onChangePage,
|
||||
addKeybinds,
|
||||
useData,
|
||||
getCount,
|
||||
getData,
|
||||
otherOperations,
|
||||
renderContent,
|
||||
zoomable,
|
||||
selectable,
|
||||
renderEditDialog,
|
||||
renderDeleteDialog,
|
||||
updateQueryParams,
|
||||
}: IListHookOptions<QueryResult, QueryData> &
|
||||
IQuery<QueryResult, QueryData> &
|
||||
IRenderListProps) => {
|
||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
const [lastClickedId, setLastClickedId] = useState<string | undefined>();
|
||||
const [zoomIndex, setZoomIndex] = useState<number>(
|
||||
options.defaultZoomIndex ?? 1
|
||||
);
|
||||
// Store initial pathname to prevent hooks from operating outside this page
|
||||
const originalPathName = useRef(location.pathname);
|
||||
const [zoomIndex, setZoomIndex] = useState<number>(defaultZoomIndex ?? 1);
|
||||
|
||||
const result = options.useData(getFilter());
|
||||
const totalCount = options.getCount(result);
|
||||
const items = options.getData(result);
|
||||
const result = useData(filter);
|
||||
const totalCount = getCount(result);
|
||||
const items = getData(result);
|
||||
|
||||
useEffect(() => {
|
||||
Mousetrap.bind("right", () => {
|
||||
|
|
@ -140,7 +169,6 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
|||
onChangePage(filter.currentPage - 1);
|
||||
}
|
||||
});
|
||||
|
||||
Mousetrap.bind("shift+right", () => {
|
||||
const maxPage = totalCount / filter.itemsPerPage + 1;
|
||||
onChangePage(Math.min(maxPage, filter.currentPage + 10));
|
||||
|
|
@ -157,8 +185,8 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
|||
});
|
||||
|
||||
let unbindExtras: () => void;
|
||||
if (options.addKeybinds) {
|
||||
unbindExtras = options.addKeybinds(result, filter, selectedIds);
|
||||
if (addKeybinds) {
|
||||
unbindExtras = addKeybinds(result, filter, selectedIds);
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
|
@ -175,102 +203,6 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
|||
};
|
||||
});
|
||||
|
||||
const updateInterfaceConfig = useCallback(
|
||||
(updatedFilter: ListFilterModel) => {
|
||||
setInterfaceState((config) => {
|
||||
const data = { ...config } as IInterfaceConfig;
|
||||
data.queries = {
|
||||
[options.filterMode]: {
|
||||
filter: updatedFilter.makeQueryParameters(),
|
||||
itemsPerPage: updatedFilter.itemsPerPage,
|
||||
currentPage: updatedFilter.currentPage,
|
||||
},
|
||||
};
|
||||
return data;
|
||||
});
|
||||
},
|
||||
[options.filterMode, setInterfaceState]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
interfaceState.loading ||
|
||||
// Only update query params on page the hook was mounted on
|
||||
history.location.pathname !== originalPathName.current
|
||||
)
|
||||
return;
|
||||
if (!forageInitialised) setForageInitialised(true);
|
||||
|
||||
if (!options.persistState) return;
|
||||
|
||||
const storedQuery = interfaceState.data?.queries?.[options.filterMode];
|
||||
if (!storedQuery) return;
|
||||
|
||||
const queryFilter = queryString.parse(history.location.search);
|
||||
const storedFilter = queryString.parse(storedQuery.filter);
|
||||
const query = history.location.search
|
||||
? {
|
||||
sortby: storedFilter.sortby,
|
||||
sortdir: storedFilter.sortdir,
|
||||
disp: storedFilter.disp,
|
||||
perPage: storedFilter.perPage,
|
||||
...queryFilter,
|
||||
}
|
||||
: storedFilter;
|
||||
|
||||
const newFilter = new ListFilterModel(options.filterMode, query);
|
||||
|
||||
// Compare constructed filter with current filter.
|
||||
// If different it's the result of navigation, and we update the filter.
|
||||
const newLocation = { ...history.location };
|
||||
newLocation.search = newFilter.makeQueryParameters();
|
||||
if (newLocation.search !== filter.makeQueryParameters()) {
|
||||
setFilter(newFilter);
|
||||
updateInterfaceConfig(newFilter);
|
||||
}
|
||||
// If constructed search is different from current, update it as well
|
||||
if (newLocation.search !== location.search) {
|
||||
newLocation.search = newFilter.makeQueryParameters();
|
||||
history.replace(newLocation);
|
||||
}
|
||||
}, [
|
||||
filter,
|
||||
interfaceState.data,
|
||||
interfaceState.loading,
|
||||
history,
|
||||
location.search,
|
||||
options.filterMode,
|
||||
forageInitialised,
|
||||
updateInterfaceConfig,
|
||||
options.persistState,
|
||||
]);
|
||||
|
||||
function getFilter() {
|
||||
if (!options.filterHook) {
|
||||
return filter;
|
||||
}
|
||||
|
||||
// make a copy of the filter and call the hook
|
||||
const newFilter = _.cloneDeep(filter);
|
||||
return options.filterHook(newFilter);
|
||||
}
|
||||
|
||||
function updateQueryParams(listFilter: ListFilterModel) {
|
||||
setFilter(listFilter);
|
||||
const newLocation = { ...location };
|
||||
newLocation.search = listFilter.makeQueryParameters();
|
||||
history.replace(newLocation);
|
||||
if (options.persistState) {
|
||||
updateInterfaceConfig(listFilter);
|
||||
}
|
||||
}
|
||||
|
||||
function onChangePage(page: number) {
|
||||
const newFilter = _.cloneDeep(filter);
|
||||
newFilter.currentPage = page;
|
||||
updateQueryParams(newFilter);
|
||||
}
|
||||
|
||||
function singleSelect(id: string, selected: boolean) {
|
||||
setLastClickedId(id);
|
||||
|
||||
|
|
@ -348,54 +280,21 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
|||
setZoomIndex(newZoomIndex);
|
||||
}
|
||||
|
||||
const otherOperations = options.otherOperations
|
||||
? options.otherOperations.map((o) => {
|
||||
return {
|
||||
text: o.text,
|
||||
onClick: () => {
|
||||
o.onClick(result, filter, selectedIds);
|
||||
},
|
||||
isDisplayed: () => {
|
||||
if (o.isDisplayed) {
|
||||
return o.isDisplayed(result, filter, selectedIds);
|
||||
}
|
||||
const operations =
|
||||
otherOperations &&
|
||||
otherOperations.map((o) => ({
|
||||
text: o.text,
|
||||
onClick: () => {
|
||||
o.onClick(result, filter, selectedIds);
|
||||
},
|
||||
isDisplayed: () => {
|
||||
if (o.isDisplayed) {
|
||||
return o.isDisplayed(result, filter, selectedIds);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
})
|
||||
: undefined;
|
||||
|
||||
function maybeRenderContent() {
|
||||
if (!result.loading && !result.error) {
|
||||
return options.renderContent(result, filter, selectedIds, zoomIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderPaginationIndex() {
|
||||
if (!result.loading && !result.error) {
|
||||
return (
|
||||
<PaginationIndex
|
||||
itemsPerPage={filter.itemsPerPage}
|
||||
currentPage={filter.currentPage}
|
||||
totalItems={totalCount}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderPagination() {
|
||||
if (!result.loading && !result.error) {
|
||||
return (
|
||||
<Pagination
|
||||
itemsPerPage={filter.itemsPerPage}
|
||||
currentPage={filter.currentPage}
|
||||
totalItems={totalCount}
|
||||
onChangePage={onChangePage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
}));
|
||||
|
||||
function onEdit() {
|
||||
setIsEditDialogOpen(true);
|
||||
|
|
@ -425,60 +324,189 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
|||
result.refetch();
|
||||
}
|
||||
|
||||
const template = (
|
||||
<div>
|
||||
<ListFilter
|
||||
onFilterUpdate={updateQueryParams}
|
||||
onSelectAll={options.selectable ? onSelectAll : undefined}
|
||||
onSelectNone={options.selectable ? onSelectNone : undefined}
|
||||
zoomIndex={options.zoomable ? zoomIndex : undefined}
|
||||
onChangeZoom={options.zoomable ? onChangeZoom : undefined}
|
||||
otherOperations={otherOperations}
|
||||
itemsSelected={selectedIds.size > 0}
|
||||
onEdit={options.renderEditDialog ? onEdit : undefined}
|
||||
onDelete={options.renderDeleteDialog ? onDelete : undefined}
|
||||
filter={filter}
|
||||
/>
|
||||
{isEditDialogOpen && options.renderEditDialog
|
||||
? options.renderEditDialog(
|
||||
options.getSelectedData(result, selectedIds),
|
||||
(applied) => onEditDialogClosed(applied)
|
||||
)
|
||||
: undefined}
|
||||
{isDeleteDialogOpen && options.renderDeleteDialog
|
||||
? options.renderDeleteDialog(
|
||||
options.getSelectedData(result, selectedIds),
|
||||
(deleted) => onDeleteDialogClosed(deleted)
|
||||
)
|
||||
: undefined}
|
||||
{(result.loading || !forageInitialised) && <LoadingIndicator />}
|
||||
{result.error && <h1>{result.error.message}</h1>}
|
||||
{maybeRenderPagination()}
|
||||
{maybeRenderContent()}
|
||||
{maybeRenderPaginationIndex()}
|
||||
{maybeRenderPagination()}
|
||||
</div>
|
||||
const renderPagination = () => (
|
||||
<Pagination
|
||||
itemsPerPage={filter.itemsPerPage}
|
||||
currentPage={filter.currentPage}
|
||||
totalItems={totalCount}
|
||||
onChangePage={onChangePage}
|
||||
/>
|
||||
);
|
||||
|
||||
return { filter, template, onSelectChange };
|
||||
let content;
|
||||
if (result.loading) {
|
||||
content = <LoadingIndicator />;
|
||||
} else if (result.error) {
|
||||
content = <h1>{result.error.message}</h1>;
|
||||
} else {
|
||||
content = (
|
||||
<div>
|
||||
<ListFilter
|
||||
onFilterUpdate={updateQueryParams}
|
||||
onSelectAll={selectable ? onSelectAll : undefined}
|
||||
onSelectNone={selectable ? onSelectNone : undefined}
|
||||
zoomIndex={zoomable ? zoomIndex : undefined}
|
||||
onChangeZoom={zoomable ? onChangeZoom : undefined}
|
||||
otherOperations={operations}
|
||||
itemsSelected={selectedIds.size > 0}
|
||||
onEdit={renderEditDialog ? onEdit : undefined}
|
||||
onDelete={renderDeleteDialog ? onDelete : undefined}
|
||||
filter={filter}
|
||||
/>
|
||||
{isEditDialogOpen &&
|
||||
renderEditDialog &&
|
||||
renderEditDialog(
|
||||
getSelectedData(getData(result), selectedIds),
|
||||
(applied) => onEditDialogClosed(applied)
|
||||
)}
|
||||
{isDeleteDialogOpen &&
|
||||
renderDeleteDialog &&
|
||||
renderDeleteDialog(
|
||||
getSelectedData(getData(result), selectedIds),
|
||||
(deleted) => onDeleteDialogClosed(deleted)
|
||||
)}
|
||||
{renderPagination()}
|
||||
{renderContent(result, filter, selectedIds, zoomIndex)}
|
||||
<PaginationIndex
|
||||
itemsPerPage={filter.itemsPerPage}
|
||||
currentPage={filter.currentPage}
|
||||
totalItems={totalCount}
|
||||
/>
|
||||
{renderPagination()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return { contentTemplate: content, onSelectChange };
|
||||
};
|
||||
|
||||
const getSelectedData = <I extends IDataItem>(
|
||||
result: I[],
|
||||
selectedIds: Set<string>
|
||||
) => {
|
||||
// find the selected items from the ids
|
||||
const selectedResults: I[] = [];
|
||||
const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
||||
options: IListHookOptions<QueryResult, QueryData> &
|
||||
IQuery<QueryResult, QueryData>
|
||||
): IListHookData => {
|
||||
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
|
||||
);
|
||||
// Store initial pathname to prevent hooks from operating outside this page
|
||||
const originalPathName = useRef(location.pathname);
|
||||
|
||||
selectedIds.forEach((id) => {
|
||||
const item = result.find((s) => s.id === id);
|
||||
const [filter, setFilter] = useState<ListFilterModel>(
|
||||
new ListFilterModel(options.filterMode, queryString.parse(location.search))
|
||||
);
|
||||
|
||||
if (item) {
|
||||
selectedResults.push(item);
|
||||
const updateInterfaceConfig = useCallback(
|
||||
(updatedFilter: ListFilterModel) => {
|
||||
setInterfaceState((config) => {
|
||||
const data = { ...config } as IInterfaceConfig;
|
||||
data.queries = {
|
||||
[options.filterMode]: {
|
||||
filter: updatedFilter.makeQueryParameters(),
|
||||
itemsPerPage: updatedFilter.itemsPerPage,
|
||||
currentPage: updatedFilter.currentPage,
|
||||
},
|
||||
};
|
||||
return data;
|
||||
});
|
||||
},
|
||||
[options.filterMode, setInterfaceState]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
interfaceState.loading ||
|
||||
// Only update query params on page the hook was mounted on
|
||||
history.location.pathname !== originalPathName.current
|
||||
)
|
||||
return;
|
||||
|
||||
if (!forageInitialised) setForageInitialised(true);
|
||||
|
||||
if (!options.persistState) return;
|
||||
|
||||
const storedQuery = interfaceState.data?.queries?.[options.filterMode];
|
||||
if (!storedQuery) return;
|
||||
|
||||
const queryFilter = queryString.parse(history.location.search);
|
||||
const storedFilter = queryString.parse(storedQuery.filter);
|
||||
const query = history.location.search
|
||||
? {
|
||||
sortby: storedFilter.sortby,
|
||||
sortdir: storedFilter.sortdir,
|
||||
disp: storedFilter.disp,
|
||||
perPage: storedFilter.perPage,
|
||||
...queryFilter,
|
||||
}
|
||||
: storedFilter;
|
||||
|
||||
const newFilter = new ListFilterModel(options.filterMode, query);
|
||||
|
||||
// Compare constructed filter with current filter.
|
||||
// If different it's the result of navigation, and we update the filter.
|
||||
const newLocation = { ...history.location };
|
||||
newLocation.search = newFilter.makeQueryParameters();
|
||||
if (newLocation.search !== filter.makeQueryParameters()) {
|
||||
setFilter(newFilter);
|
||||
updateInterfaceConfig(newFilter);
|
||||
}
|
||||
// If constructed search is different from current, update it as well
|
||||
if (newLocation.search !== location.search) {
|
||||
newLocation.search = newFilter.makeQueryParameters();
|
||||
history.replace(newLocation);
|
||||
}
|
||||
}, [
|
||||
filter,
|
||||
interfaceState.data,
|
||||
interfaceState.loading,
|
||||
history,
|
||||
location.search,
|
||||
options.filterMode,
|
||||
forageInitialised,
|
||||
updateInterfaceConfig,
|
||||
options.persistState,
|
||||
]);
|
||||
|
||||
function updateQueryParams(listFilter: ListFilterModel) {
|
||||
setFilter(listFilter);
|
||||
const newLocation = { ...location };
|
||||
newLocation.search = listFilter.makeQueryParameters();
|
||||
history.replace(newLocation);
|
||||
if (options.persistState) {
|
||||
updateInterfaceConfig(listFilter);
|
||||
}
|
||||
}
|
||||
|
||||
const onChangePage = (page: number) => {
|
||||
const newFilter = _.cloneDeep(filter);
|
||||
newFilter.currentPage = page;
|
||||
updateQueryParams(newFilter);
|
||||
};
|
||||
|
||||
const renderFilter = !options.filterHook
|
||||
? filter
|
||||
: options.filterHook(_.cloneDeep(filter));
|
||||
|
||||
const { contentTemplate, onSelectChange } = RenderList({
|
||||
...options,
|
||||
filter: renderFilter,
|
||||
onChangePage,
|
||||
updateQueryParams,
|
||||
});
|
||||
|
||||
return selectedResults;
|
||||
const template = !forageInitialised ? (
|
||||
<LoadingIndicator />
|
||||
) : (
|
||||
<>{contentTemplate}</>
|
||||
);
|
||||
|
||||
return {
|
||||
filter,
|
||||
template,
|
||||
onSelectChange,
|
||||
};
|
||||
};
|
||||
|
||||
export const useScenesList = (
|
||||
|
|
@ -492,10 +520,6 @@ export const useScenesList = (
|
|||
result?.data?.findScenes?.scenes ?? [],
|
||||
getCount: (result: FindScenesQueryResult) =>
|
||||
result?.data?.findScenes?.count ?? 0,
|
||||
getSelectedData: (
|
||||
result: FindScenesQueryResult,
|
||||
selectedIds: Set<string>
|
||||
) => getSelectedData(result?.data?.findScenes?.scenes ?? [], selectedIds),
|
||||
});
|
||||
|
||||
export const useSceneMarkersList = (
|
||||
|
|
@ -509,14 +533,6 @@ export const useSceneMarkersList = (
|
|||
result?.data?.findSceneMarkers?.scene_markers ?? [],
|
||||
getCount: (result: FindSceneMarkersQueryResult) =>
|
||||
result?.data?.findSceneMarkers?.count ?? 0,
|
||||
getSelectedData: (
|
||||
result: FindSceneMarkersQueryResult,
|
||||
selectedIds: Set<string>
|
||||
) =>
|
||||
getSelectedData(
|
||||
result?.data?.findSceneMarkers?.scene_markers ?? [],
|
||||
selectedIds
|
||||
),
|
||||
});
|
||||
|
||||
export const useGalleriesList = (
|
||||
|
|
@ -530,14 +546,6 @@ export const useGalleriesList = (
|
|||
result?.data?.findGalleries?.galleries ?? [],
|
||||
getCount: (result: FindGalleriesQueryResult) =>
|
||||
result?.data?.findGalleries?.count ?? 0,
|
||||
getSelectedData: (
|
||||
result: FindGalleriesQueryResult,
|
||||
selectedIds: Set<string>
|
||||
) =>
|
||||
getSelectedData(
|
||||
result?.data?.findGalleries?.galleries ?? [],
|
||||
selectedIds
|
||||
),
|
||||
});
|
||||
|
||||
export const useStudiosList = (
|
||||
|
|
@ -551,10 +559,6 @@ export const useStudiosList = (
|
|||
result?.data?.findStudios?.studios ?? [],
|
||||
getCount: (result: FindStudiosQueryResult) =>
|
||||
result?.data?.findStudios?.count ?? 0,
|
||||
getSelectedData: (
|
||||
result: FindStudiosQueryResult,
|
||||
selectedIds: Set<string>
|
||||
) => getSelectedData(result?.data?.findStudios?.studios ?? [], selectedIds),
|
||||
});
|
||||
|
||||
export const usePerformersList = (
|
||||
|
|
@ -568,14 +572,6 @@ export const usePerformersList = (
|
|||
result?.data?.findPerformers?.performers ?? [],
|
||||
getCount: (result: FindPerformersQueryResult) =>
|
||||
result?.data?.findPerformers?.count ?? 0,
|
||||
getSelectedData: (
|
||||
result: FindPerformersQueryResult,
|
||||
selectedIds: Set<string>
|
||||
) =>
|
||||
getSelectedData(
|
||||
result?.data?.findPerformers?.performers ?? [],
|
||||
selectedIds
|
||||
),
|
||||
});
|
||||
|
||||
export const useMoviesList = (
|
||||
|
|
@ -589,10 +585,6 @@ export const useMoviesList = (
|
|||
result?.data?.findMovies?.movies ?? [],
|
||||
getCount: (result: FindMoviesQueryResult) =>
|
||||
result?.data?.findMovies?.count ?? 0,
|
||||
getSelectedData: (
|
||||
result: FindMoviesQueryResult,
|
||||
selectedIds: Set<string>
|
||||
) => getSelectedData(result?.data?.findMovies?.movies ?? [], selectedIds),
|
||||
});
|
||||
|
||||
export const useTagsList = (
|
||||
|
|
@ -606,13 +598,11 @@ export const useTagsList = (
|
|||
result?.data?.findTags?.tags ?? [],
|
||||
getCount: (result: FindTagsQueryResult) =>
|
||||
result?.data?.findTags?.count ?? 0,
|
||||
getSelectedData: (result: FindTagsQueryResult, selectedIds: Set<string>) =>
|
||||
getSelectedData(result?.data?.findTags?.tags ?? [], selectedIds),
|
||||
});
|
||||
|
||||
export const showWhenSelected = (
|
||||
result: FindScenesQueryResult,
|
||||
filter: ListFilterModel,
|
||||
_result: FindScenesQueryResult,
|
||||
_filter: ListFilterModel,
|
||||
selectedIds: Set<string>
|
||||
) => {
|
||||
return selectedIds.size > 0;
|
||||
|
|
|
|||
|
|
@ -26,10 +26,7 @@ export const useVideoHover = (options: IVideoHoverHookOptions) => {
|
|||
return;
|
||||
}
|
||||
if (videoTag.paused && !isPlaying.current) {
|
||||
videoTag.play().catch((error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error.message);
|
||||
});
|
||||
videoTag.play().catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -176,10 +176,14 @@ hr {
|
|||
color: $text-color;
|
||||
}
|
||||
|
||||
.modal-header,
|
||||
.modal-body,
|
||||
.modal-footer {
|
||||
&-header,
|
||||
&-body,
|
||||
&-footer {
|
||||
background-color: #30404d;
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
&-content {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue