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:
InfiniteTF
2020-08-28 08:33:19 +02:00
committed by GitHub
parent 9a84726128
commit fef16d7e09
16 changed files with 345 additions and 325 deletions

View File

@@ -132,7 +132,7 @@ export const Manual: React.FC<IManualProps> = ({ show, onClose }) => {
<Nav variant="pills" className="flex-column"> <Nav variant="pills" className="flex-column">
{content.map((c) => { {content.map((c) => {
return ( return (
<Nav.Item> <Nav.Item key={`${c.key}-nav`}>
<Nav.Link className={c.className} eventKey={c.key}> <Nav.Link className={c.className} eventKey={c.key}>
{c.title} {c.title}
</Nav.Link> </Nav.Link>
@@ -146,7 +146,11 @@ export const Manual: React.FC<IManualProps> = ({ show, onClose }) => {
<Tab.Content> <Tab.Content>
{content.map((c) => { {content.map((c) => {
return ( return (
<Tab.Pane eventKey={c.key} onClick={interceptLinkClick}> <Tab.Pane
eventKey={c.key}
key={`${c.key}-pane`}
onClick={interceptLinkClick}
>
<Page page={c.content} /> <Page page={c.content} />
</Tab.Pane> </Tab.Pane>
); );

View File

@@ -1,18 +1,17 @@
.manual { .manual {
background-color: #30404d;
color: $text-color; color: $text-color;
.close { .close {
color: $text-color; color: $text-color;
} }
.manual-container { &-container {
padding-left: 1px; padding-left: 1px;
padding-right: 5px; padding-right: 5px;
} }
.modal-header, &-header,
.modal-body { &-body {
background-color: #30404d; background-color: #30404d;
color: $text-color; color: $text-color;
overflow-y: hidden; overflow-y: hidden;
@@ -21,22 +20,22 @@
.indent-1 { .indent-1 {
padding-left: 2rem; padding-left: 2rem;
} }
}
.manual .manual-content, .manual-content,
.manual .manual-toc { .manual-toc {
max-height: calc(100vh - 10rem); max-height: calc(100vh - 10rem);
overflow-y: auto;
}
@media (max-width: 992px) {
.manual .modal-body {
overflow-y: auto; overflow-y: auto;
}
.manual-content, @media (max-width: 992px) {
.manual-toc { .modal-body {
max-height: inherit; overflow-y: auto;
overflow-y: hidden;
.manual-content,
.manual-toc {
max-height: inherit;
overflow-y: hidden;
}
} }
} }
} }

View File

@@ -285,6 +285,10 @@ export const Performer: React.FC = () => {
const photos = [{ src: activeImage, caption: "Image" }]; const photos = [{ src: activeImage, caption: "Image" }];
if (!performer.id) {
return <LoadingIndicator />;
}
return ( return (
<div id="performer-page" className="row"> <div id="performer-page" className="row">
<div className="image-container col-md-4 text-center"> <div className="image-container col-md-4 text-center">

View File

@@ -102,7 +102,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
if (props.scene.performers.length <= 0) return; if (props.scene.performers.length <= 0) return;
const popoverContent = props.scene.performers.map((performer) => ( 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 <Link
to={`/performers/${performer.id}`} to={`/performers/${performer.id}`}
className="performer-tag col m-auto zoom-2" className="performer-tag col m-auto zoom-2"
@@ -151,7 +151,11 @@ export const SceneCard: React.FC<ISceneCardProps> = (
)); ));
return ( return (
<HoverPopover placement="bottom" content={popoverContent}> <HoverPopover
placement="bottom"
content={popoverContent}
className="tag-tooltip"
>
<Button className="minimal"> <Button className="minimal">
<Icon icon="film" /> <Icon icon="film" />
<span>{props.scene.movies.length}</span> <span>{props.scene.movies.length}</span>
@@ -285,26 +289,28 @@ export const SceneCard: React.FC<ISceneCardProps> = (
}} }}
/> />
<Link <div className="video-section">
to={`/scenes/${props.scene.id}`} <Link
className="scene-card-link" to={`/scenes/${props.scene.id}`}
onClick={handleSceneClick} className="scene-card-link"
onDragStart={handleDrag} onClick={handleSceneClick}
onDragOver={handleDragOver} onDragStart={handleDrag}
draggable={props.selecting} 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}
> >
{previewPath ? <source src={previewPath} /> : ""} {maybeRenderRatingBanner()}
</video> {maybeRenderSceneSpecsOverlay()}
</Link> <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"> <div className="card-section">
<h5 className="card-section-title"> <h5 className="card-section-title">
{props.scene.title {props.scene.title

View File

@@ -99,6 +99,7 @@ export const RatingStars: React.FC<IRatingStarsProps> = (
onFocus={() => onMouseOver(rating)} onFocus={() => onMouseOver(rating)}
onBlur={() => onMouseOut(rating)} onBlur={() => onMouseOut(rating)}
title={getTooltip(rating)} title={getTooltip(rating)}
key={`star-${rating}`}
> >
<Icon <Icon
icon={[getIconPrefix(rating), "star"]} icon={[getIconPrefix(rating), "star"]}

View File

@@ -13,6 +13,10 @@
} }
} }
.video-section {
position: relative;
}
.card-section { .card-section {
margin-bottom: 0; margin-bottom: 0;
padding: 0.5rem 1rem 0 1rem; padding: 0.5rem 1rem 0 1rem;
@@ -174,10 +178,6 @@ textarea.scene-description {
padding: 0; padding: 0;
} }
&-link {
position: relative;
}
.scene-card-check { .scene-card-check {
left: 0.5rem; left: 0.5rem;
margin-top: -12px; margin-top: -12px;

View File

@@ -455,8 +455,10 @@ export const SettingsConfigurationPanel: React.FC = () => {
className="col col-sm-6 text-input" className="col col-sm-6 text-input"
type="number" type="number"
value={previewSegments.toString()} value={previewSegments.toString()}
onInput={(e: React.FormEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setPreviewSegments(Number.parseInt(e.currentTarget.value, 10)) setPreviewSegments(
Number.parseInt(e.currentTarget.value || "0", 10)
)
} }
/> />
<Form.Text className="text-muted"> <Form.Text className="text-muted">
@@ -470,9 +472,9 @@ export const SettingsConfigurationPanel: React.FC = () => {
className="col col-sm-6 text-input" className="col col-sm-6 text-input"
type="number" type="number"
value={previewSegmentDuration.toString()} value={previewSegmentDuration.toString()}
onInput={(e: React.FormEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setPreviewSegmentDuration( 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" className="col col-sm-6 text-input"
type="number" type="number"
value={maxSessionAge.toString()} value={maxSessionAge.toString()}
onInput={(e: React.FormEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setMaxSessionAge(Number.parseInt(e.currentTarget.value, 10)) setMaxSessionAge(
Number.parseInt(e.currentTarget.value || "0", 10)
)
} }
/> />
<Form.Text className="text-muted"> <Form.Text className="text-muted">

View File

@@ -66,6 +66,7 @@ export const HoverPopover: React.FC<IHoverPopover> = ({
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
id="popover" id="popover"
className="hover-popover-content"
> >
{content} {content}
</Popover> </Popover>

View File

@@ -1,6 +1,7 @@
import { Badge } from "react-bootstrap"; import { Badge } from "react-bootstrap";
import React from "react"; import React from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import cx from "classnames";
import { import {
PerformerDataFragment, PerformerDataFragment,
SceneMarkerDataFragment, SceneMarkerDataFragment,
@@ -43,7 +44,7 @@ export const TagLink: React.FC<IProps> = (props: IProps) => {
: TextUtils.fileNameFromPath(props.scene.path ?? ""); : TextUtils.fileNameFromPath(props.scene.path ?? "");
} }
return ( return (
<Badge className={`tag-item ${props.className}`} variant="secondary"> <Badge className={cx("tag-item", props.className)} variant="secondary">
<Link to={link}>{title}</Link> <Link to={link}>{title}</Link>
</Badge> </Badge>
); );

View File

@@ -128,3 +128,8 @@ button.collapse-button.btn-primary:not(:disabled):not(.disabled):active {
box-shadow: none; box-shadow: none;
color: #f5f8fa; color: #f5f8fa;
} }
.hover-popover-content {
max-width: 32rem;
text-align: center;
}

View File

@@ -102,7 +102,7 @@ export const Studio: React.FC = () => {
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, isEditing); const imageEncoding = ImageUtils.usePasteImage(onImageLoad, isEditing);
if (!isNew && !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>; if (error) return <div>{error.message}</div>;
} }

View File

@@ -36,7 +36,7 @@ export const Tag: React.FC = () => {
const [name, setName] = useState<string>(); const [name, setName] = useState<string>();
// Tag state // Tag state
const [tag, setTag] = useState<Partial<GQL.TagDataFragment>>({}); const [tag, setTag] = useState<GQL.TagDataFragment | undefined>();
const [imagePreview, setImagePreview] = useState<string>(); const [imagePreview, setImagePreview] = useState<string>();
const { data, error, loading } = useFindTag(id); 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); setName(state.name);
} }
function updateTagData(tagData: Partial<GQL.TagDataFragment>) { function updateTagData(tagData: GQL.TagDataFragment) {
setImage(undefined); setImage(undefined);
updateTagEditState(tagData); updateTagEditState(tagData);
setImagePreview(tagData.image_path ?? undefined); setImagePreview(tagData.image_path ?? undefined);
@@ -104,15 +104,17 @@ export const Tag: React.FC = () => {
} }
function getTagInput() { function getTagInput() {
const input: Partial<GQL.TagCreateInput | GQL.TagUpdateInput> = { if (!isNew) {
return {
id,
name,
image,
};
}
return {
name, name,
image, image,
}; };
if (!isNew) {
(input as GQL.TagUpdateInput).id = id;
}
return input;
} }
async function onSave() { async function onSave() {
@@ -136,7 +138,7 @@ export const Tag: React.FC = () => {
} }
async function onAutoTag() { async function onAutoTag() {
if (!tag.id) return; if (!tag?.id) return;
try { try {
await mutateMetadataAutoTag({ tags: [tag.id] }); await mutateMetadataAutoTag({ tags: [tag.id] });
Toast.success({ content: "Started auto tagging" }); Toast.success({ content: "Started auto tagging" });
@@ -175,13 +177,15 @@ export const Tag: React.FC = () => {
function onToggleEdit() { function onToggleEdit() {
setIsEditing(!isEditing); setIsEditing(!isEditing);
updateTagData(tag); if (tag) {
updateTagData(tag);
}
} }
function onClearImage() { function onClearImage() {
setImage(null); setImage(null);
setImagePreview( 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 acceptSVG
/> />
</div> </div>
{!isNew && ( {!isNew && tag && (
<div className="col col-md-8"> <div className="col col-md-8">
<Tabs <Tabs
id="tag-tabs" id="tag-tabs"

View File

@@ -5,12 +5,12 @@ import { SceneList } from "src/components/Scenes/SceneList";
import { TagsCriterion } from "src/models/list-filter/criteria/tags"; import { TagsCriterion } from "src/models/list-filter/criteria/tags";
interface ITagScenesPanel { interface ITagScenesPanel {
tag: Partial<GQL.TagDataFragment>; tag: GQL.TagDataFragment;
} }
export const TagScenesPanel: React.FC<ITagScenesPanel> = ({ tag }) => { export const TagScenesPanel: React.FC<ITagScenesPanel> = ({ tag }) => {
function filterHook(filter: ListFilterModel) { 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 // if tag is already present, then we modify it, otherwise add
let tagCriterion = filter.criteria.find((c) => { let tagCriterion = filter.criteria.find((c) => {
return c.type === "tags"; return c.type === "tags";

View File

@@ -38,6 +38,24 @@ import {
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
import { FilterMode } from "src/models/list-filter/types"; 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 { interface IListHookData {
filter: ListFilterModel; filter: ListFilterModel;
template: JSX.Element; template: JSX.Element;
@@ -99,34 +117,45 @@ interface IQuery<T extends IQueryResult, T2 extends IDataItem> {
filterMode: FilterMode; filterMode: FilterMode;
useData: (filter: ListFilterModel) => T; useData: (filter: ListFilterModel) => T;
getData: (data: T) => T2[]; getData: (data: T) => T2[];
getSelectedData: (data: T, selectedIds: Set<string>) => T2[];
getCount: (data: T) => number; getCount: (data: T) => number;
} }
const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>( interface IRenderListProps {
options: IListHookOptions<QueryResult, QueryData> & filter: ListFilterModel;
IQuery<QueryResult, QueryData> onChangePage: (page: number) => void;
): IListHookData => { updateQueryParams: (filter: ListFilterModel) => void;
const [interfaceState, setInterfaceState] = useInterfaceLocalForage(); }
const [forageInitialised, setForageInitialised] = useState(false);
const history = useHistory(); const RenderList = <
const location = useLocation(); QueryResult extends IQueryResult,
const [filter, setFilter] = useState<ListFilterModel>( QueryData extends IDataItem
new ListFilterModel(options.filterMode, queryString.parse(location.search)) >({
); 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 [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set()); const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [lastClickedId, setLastClickedId] = useState<string | undefined>(); const [lastClickedId, setLastClickedId] = useState<string | undefined>();
const [zoomIndex, setZoomIndex] = useState<number>( const [zoomIndex, setZoomIndex] = useState<number>(defaultZoomIndex ?? 1);
options.defaultZoomIndex ?? 1
);
// Store initial pathname to prevent hooks from operating outside this page
const originalPathName = useRef(location.pathname);
const result = options.useData(getFilter()); const result = useData(filter);
const totalCount = options.getCount(result); const totalCount = getCount(result);
const items = options.getData(result); const items = getData(result);
useEffect(() => { useEffect(() => {
Mousetrap.bind("right", () => { Mousetrap.bind("right", () => {
@@ -140,7 +169,6 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
onChangePage(filter.currentPage - 1); onChangePage(filter.currentPage - 1);
} }
}); });
Mousetrap.bind("shift+right", () => { Mousetrap.bind("shift+right", () => {
const maxPage = totalCount / filter.itemsPerPage + 1; const maxPage = totalCount / filter.itemsPerPage + 1;
onChangePage(Math.min(maxPage, filter.currentPage + 10)); onChangePage(Math.min(maxPage, filter.currentPage + 10));
@@ -157,8 +185,8 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
}); });
let unbindExtras: () => void; let unbindExtras: () => void;
if (options.addKeybinds) { if (addKeybinds) {
unbindExtras = options.addKeybinds(result, filter, selectedIds); unbindExtras = addKeybinds(result, filter, selectedIds);
} }
return () => { 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) { function singleSelect(id: string, selected: boolean) {
setLastClickedId(id); setLastClickedId(id);
@@ -348,54 +280,21 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
setZoomIndex(newZoomIndex); setZoomIndex(newZoomIndex);
} }
const otherOperations = options.otherOperations const operations =
? options.otherOperations.map((o) => { otherOperations &&
return { otherOperations.map((o) => ({
text: o.text, text: o.text,
onClick: () => { onClick: () => {
o.onClick(result, filter, selectedIds); o.onClick(result, filter, selectedIds);
}, },
isDisplayed: () => { isDisplayed: () => {
if (o.isDisplayed) { if (o.isDisplayed) {
return o.isDisplayed(result, filter, selectedIds); return o.isDisplayed(result, filter, selectedIds);
} }
return true; 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}
/>
);
}
}
function onEdit() { function onEdit() {
setIsEditDialogOpen(true); setIsEditDialogOpen(true);
@@ -425,60 +324,189 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
result.refetch(); result.refetch();
} }
const template = ( const renderPagination = () => (
<div> <Pagination
<ListFilter itemsPerPage={filter.itemsPerPage}
onFilterUpdate={updateQueryParams} currentPage={filter.currentPage}
onSelectAll={options.selectable ? onSelectAll : undefined} totalItems={totalCount}
onSelectNone={options.selectable ? onSelectNone : undefined} onChangePage={onChangePage}
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>
); );
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>( const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
result: I[], options: IListHookOptions<QueryResult, QueryData> &
selectedIds: Set<string> IQuery<QueryResult, QueryData>
) => { ): IListHookData => {
// find the selected items from the ids const history = useHistory();
const selectedResults: I[] = []; 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 [filter, setFilter] = useState<ListFilterModel>(
const item = result.find((s) => s.id === id); new ListFilterModel(options.filterMode, queryString.parse(location.search))
);
if (item) { const updateInterfaceConfig = useCallback(
selectedResults.push(item); (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 = ( export const useScenesList = (
@@ -492,10 +520,6 @@ export const useScenesList = (
result?.data?.findScenes?.scenes ?? [], result?.data?.findScenes?.scenes ?? [],
getCount: (result: FindScenesQueryResult) => getCount: (result: FindScenesQueryResult) =>
result?.data?.findScenes?.count ?? 0, result?.data?.findScenes?.count ?? 0,
getSelectedData: (
result: FindScenesQueryResult,
selectedIds: Set<string>
) => getSelectedData(result?.data?.findScenes?.scenes ?? [], selectedIds),
}); });
export const useSceneMarkersList = ( export const useSceneMarkersList = (
@@ -509,14 +533,6 @@ export const useSceneMarkersList = (
result?.data?.findSceneMarkers?.scene_markers ?? [], result?.data?.findSceneMarkers?.scene_markers ?? [],
getCount: (result: FindSceneMarkersQueryResult) => getCount: (result: FindSceneMarkersQueryResult) =>
result?.data?.findSceneMarkers?.count ?? 0, result?.data?.findSceneMarkers?.count ?? 0,
getSelectedData: (
result: FindSceneMarkersQueryResult,
selectedIds: Set<string>
) =>
getSelectedData(
result?.data?.findSceneMarkers?.scene_markers ?? [],
selectedIds
),
}); });
export const useGalleriesList = ( export const useGalleriesList = (
@@ -530,14 +546,6 @@ export const useGalleriesList = (
result?.data?.findGalleries?.galleries ?? [], result?.data?.findGalleries?.galleries ?? [],
getCount: (result: FindGalleriesQueryResult) => getCount: (result: FindGalleriesQueryResult) =>
result?.data?.findGalleries?.count ?? 0, result?.data?.findGalleries?.count ?? 0,
getSelectedData: (
result: FindGalleriesQueryResult,
selectedIds: Set<string>
) =>
getSelectedData(
result?.data?.findGalleries?.galleries ?? [],
selectedIds
),
}); });
export const useStudiosList = ( export const useStudiosList = (
@@ -551,10 +559,6 @@ export const useStudiosList = (
result?.data?.findStudios?.studios ?? [], result?.data?.findStudios?.studios ?? [],
getCount: (result: FindStudiosQueryResult) => getCount: (result: FindStudiosQueryResult) =>
result?.data?.findStudios?.count ?? 0, result?.data?.findStudios?.count ?? 0,
getSelectedData: (
result: FindStudiosQueryResult,
selectedIds: Set<string>
) => getSelectedData(result?.data?.findStudios?.studios ?? [], selectedIds),
}); });
export const usePerformersList = ( export const usePerformersList = (
@@ -568,14 +572,6 @@ export const usePerformersList = (
result?.data?.findPerformers?.performers ?? [], result?.data?.findPerformers?.performers ?? [],
getCount: (result: FindPerformersQueryResult) => getCount: (result: FindPerformersQueryResult) =>
result?.data?.findPerformers?.count ?? 0, result?.data?.findPerformers?.count ?? 0,
getSelectedData: (
result: FindPerformersQueryResult,
selectedIds: Set<string>
) =>
getSelectedData(
result?.data?.findPerformers?.performers ?? [],
selectedIds
),
}); });
export const useMoviesList = ( export const useMoviesList = (
@@ -589,10 +585,6 @@ export const useMoviesList = (
result?.data?.findMovies?.movies ?? [], result?.data?.findMovies?.movies ?? [],
getCount: (result: FindMoviesQueryResult) => getCount: (result: FindMoviesQueryResult) =>
result?.data?.findMovies?.count ?? 0, result?.data?.findMovies?.count ?? 0,
getSelectedData: (
result: FindMoviesQueryResult,
selectedIds: Set<string>
) => getSelectedData(result?.data?.findMovies?.movies ?? [], selectedIds),
}); });
export const useTagsList = ( export const useTagsList = (
@@ -606,13 +598,11 @@ export const useTagsList = (
result?.data?.findTags?.tags ?? [], result?.data?.findTags?.tags ?? [],
getCount: (result: FindTagsQueryResult) => getCount: (result: FindTagsQueryResult) =>
result?.data?.findTags?.count ?? 0, result?.data?.findTags?.count ?? 0,
getSelectedData: (result: FindTagsQueryResult, selectedIds: Set<string>) =>
getSelectedData(result?.data?.findTags?.tags ?? [], selectedIds),
}); });
export const showWhenSelected = ( export const showWhenSelected = (
result: FindScenesQueryResult, _result: FindScenesQueryResult,
filter: ListFilterModel, _filter: ListFilterModel,
selectedIds: Set<string> selectedIds: Set<string>
) => { ) => {
return selectedIds.size > 0; return selectedIds.size > 0;

View File

@@ -26,10 +26,7 @@ export const useVideoHover = (options: IVideoHoverHookOptions) => {
return; return;
} }
if (videoTag.paused && !isPlaying.current) { if (videoTag.paused && !isPlaying.current) {
videoTag.play().catch((error) => { videoTag.play().catch(() => {});
// eslint-disable-next-line no-console
console.log(error.message);
});
} }
}; };

View File

@@ -176,10 +176,14 @@ hr {
color: $text-color; color: $text-color;
} }
.modal-header, &-header,
.modal-body, &-body,
.modal-footer { &-footer {
background-color: #30404d; background-color: #30404d;
color: $text-color; color: $text-color;
} }
&-content {
background-color: transparent;
}
} }