Support setting galleries in multiple images (#4608)

This commit is contained in:
WithoutPants
2024-02-23 11:42:04 +11:00
committed by GitHub
parent a302fc78ea
commit 4b84ec0d85
5 changed files with 137 additions and 134 deletions

View File

@@ -32,6 +32,7 @@ import {
Criterion,
CriterionValue,
} from "src/models/list-filter/criteria/criterion";
import { PathCriterion } from "src/models/list-filter/criteria/path";
export type Gallery = Pick<GQL.Gallery, "id" | "title"> & {
files: Pick<GQL.GalleryFile, "path">[];
@@ -39,14 +40,14 @@ export type Gallery = Pick<GQL.Gallery, "id" | "title"> & {
};
type Option = SelectOption<Gallery>;
const _GallerySelect: React.FC<
IFilterProps &
IFilterValueProps<Gallery> & {
type ExtraGalleryProps = {
hoverPlacement?: Placement;
excludeIds?: string[];
} & {
extraCriteria?: Array<Criterion<CriterionValue>>;
}
};
const _GallerySelect: React.FC<
IFilterProps & IFilterValueProps<Gallery> & ExtraGalleryProps
> = (props) => {
const { configuration } = React.useContext(ConfigurationContext);
const intl = useIntl();
@@ -187,9 +188,9 @@ const _GallerySelect: React.FC<
export const GallerySelect = PatchComponent("GallerySelect", _GallerySelect);
const _GalleryIDSelect: React.FC<IFilterProps & IFilterIDProps<Gallery>> = (
props
) => {
const _GalleryIDSelect: React.FC<
IFilterProps & IFilterIDProps<Gallery> & ExtraGalleryProps
> = (props) => {
const { ids, onSelect: onSelectValues } = props;
const [values, setValues] = useState<Gallery[]>([]);
@@ -238,3 +239,11 @@ export const GalleryIDSelect = PatchComponent(
"GalleryIDSelect",
_GalleryIDSelect
);
function getExcludeFilebaseGalleriesFilter() {
const ret = new PathCriterion();
ret.modifier = GQL.CriterionModifier.IsNull;
return ret;
}
export const excludeFileBasedGalleries = [getExcludeFilebaseGalleriesFilter()];

View File

@@ -11,6 +11,7 @@ import * as FormUtils from "src/utils/form";
import { MultiSet } from "../Shared/MultiSet";
import { RatingSystem } from "../Shared/Rating/RatingSystem";
import {
getAggregateGalleryIds,
getAggregateInputIDs,
getAggregateInputValue,
getAggregatePerformerIds,
@@ -36,11 +37,19 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
const [performerIds, setPerformerIds] = useState<string[]>();
const [existingPerformerIds, setExistingPerformerIds] = useState<string[]>();
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(
GQL.BulkUpdateIdMode.Add
);
const [tagIds, setTagIds] = useState<string[]>();
const [existingTagIds, setExistingTagIds] = useState<string[]>();
const [galleryMode, setGalleryMode] = React.useState<GQL.BulkUpdateIdMode>(
GQL.BulkUpdateIdMode.Add
);
const [galleryIds, setGalleryIds] = useState<string[]>();
const [existingGalleryIds, setExistingGalleryIds] = useState<string[]>();
const [organized, setOrganized] = useState<boolean | undefined>();
const [updateImages] = useBulkImageUpdate();
@@ -56,6 +65,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
const aggregateStudioId = getAggregateStudioId(props.selected);
const aggregatePerformerIds = getAggregatePerformerIds(props.selected);
const aggregateTagIds = getAggregateTagIds(props.selected);
const aggregateGalleryIds = getAggregateGalleryIds(props.selected);
const imageInput: GQL.BulkImageUpdateInput = {
ids: props.selected.map((image) => {
@@ -72,6 +82,11 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
aggregatePerformerIds
);
imageInput.tag_ids = getAggregateInputIDs(tagMode, tagIds, aggregateTagIds);
imageInput.gallery_ids = getAggregateInputIDs(
galleryMode,
galleryIds,
aggregateGalleryIds
);
if (organized !== undefined) {
imageInput.organized = organized;
@@ -107,6 +122,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
let updateStudioID: string | undefined;
let updatePerformerIds: string[] = [];
let updateTagIds: string[] = [];
let updateGalleryIds: string[] = [];
let updateOrganized: boolean | undefined;
let first = true;
@@ -117,12 +133,14 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
.map((p) => p.id)
.sort();
const imageTagIDs = (image.tags ?? []).map((p) => p.id).sort();
const imageGalleryIDs = (image.galleries ?? []).map((p) => p.id).sort();
if (first) {
updateRating = imageRating ?? undefined;
updateStudioID = imageStudioID;
updatePerformerIds = imagePerformerIDs;
updateTagIds = imageTagIDs;
updateGalleryIds = imageGalleryIDs;
updateOrganized = image.organized;
first = false;
} else {
@@ -138,6 +156,9 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
if (!isEqual(imageTagIDs, updateTagIds)) {
updateTagIds = [];
}
if (!isEqual(imageGalleryIDs, updateGalleryIds)) {
updateGalleryIds = [];
}
if (image.organized !== updateOrganized) {
updateOrganized = undefined;
}
@@ -148,6 +169,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
setStudioId(updateStudioID);
setExistingPerformerIds(updatePerformerIds);
setExistingTagIds(updateTagIds);
setExistingGalleryIds(updateGalleryIds);
setOrganized(updateOrganized);
}, [props.selected, performerMode, tagMode]);
@@ -157,54 +179,6 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
}
}, [organized, checkboxRef]);
function renderMultiSelect(
type: "performers" | "tags",
ids: string[] | undefined
) {
let mode = GQL.BulkUpdateIdMode.Add;
let existingIds: string[] | undefined = [];
switch (type) {
case "performers":
mode = performerMode;
existingIds = existingPerformerIds;
break;
case "tags":
mode = tagMode;
existingIds = existingTagIds;
break;
}
return (
<MultiSet
type={type}
disabled={isUpdating}
onUpdate={(itemIDs) => {
switch (type) {
case "performers":
setPerformerIds(itemIDs);
break;
case "tags":
setTagIds(itemIDs);
break;
}
}}
onSetMode={(newMode) => {
switch (type) {
case "performers":
setPerformerMode(newMode);
break;
case "tags":
setTagMode(newMode);
break;
}
}}
existingIds={existingIds ?? []}
ids={ids ?? []}
mode={mode}
/>
);
}
function cycleOrganized() {
if (organized) {
setOrganized(undefined);
@@ -271,14 +245,45 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
<Form.Label>
<FormattedMessage id="performers" />
</Form.Label>
{renderMultiSelect("performers", performerIds)}
<MultiSet
type="performers"
disabled={isUpdating}
onUpdate={(itemIDs) => setPerformerIds(itemIDs)}
onSetMode={(newMode) => setPerformerMode(newMode)}
existingIds={existingPerformerIds ?? []}
ids={performerIds ?? []}
mode={performerMode}
/>
</Form.Group>
<Form.Group controlId="tags">
<Form.Label>
<FormattedMessage id="tags" />
</Form.Label>
{renderMultiSelect("tags", tagIds)}
<MultiSet
type="tags"
disabled={isUpdating}
onUpdate={(itemIDs) => setTagIds(itemIDs)}
onSetMode={(newMode) => setTagMode(newMode)}
existingIds={existingTagIds ?? []}
ids={tagIds ?? []}
mode={tagMode}
/>
</Form.Group>
<Form.Group controlId="galleries">
<Form.Label>
<FormattedMessage id="galleries" />
</Form.Label>
<MultiSet
type="galleries"
disabled={isUpdating}
onUpdate={(itemIDs) => setGalleryIds(itemIDs)}
onSetMode={(newMode) => setGalleryMode(newMode)}
existingIds={existingGalleryIds ?? []}
ids={galleryIds ?? []}
mode={galleryMode}
/>
</Form.Group>
<Form.Group controlId="organized">

View File

@@ -24,8 +24,11 @@ import { formikUtils } from "src/utils/form";
import { Tag, TagSelect } from "src/components/Tags/TagSelect";
import { Studio, StudioSelect } from "src/components/Studios/StudioSelect";
import { galleryTitle } from "src/core/galleries";
import { Gallery, GallerySelect } from "src/components/Galleries/GallerySelect";
import { PathCriterion } from "src/models/list-filter/criteria/path";
import {
Gallery,
GallerySelect,
excludeFileBasedGalleries,
} from "src/components/Galleries/GallerySelect";
interface IProps {
image: GQL.ImageDataFragment;
@@ -34,14 +37,6 @@ interface IProps {
onDelete: () => void;
}
function getExcludeFilebaseGalleriesFilter() {
const ret = new PathCriterion();
ret.modifier = GQL.CriterionModifier.IsNull;
return ret;
}
const excludeFileBasedGalleries = [getExcludeFilebaseGalleriesFilter()];
export const ImageEditPanel: React.FC<IProps> = ({
image,
isVisible,

View File

@@ -4,9 +4,13 @@ import { useIntl } from "react-intl";
import * as GQL from "src/core/generated-graphql";
import { Button, ButtonGroup } from "react-bootstrap";
import { FilterSelect, SelectObject } from "./Select";
import {
GalleryIDSelect,
excludeFileBasedGalleries,
} from "../Galleries/GallerySelect";
interface IMultiSetProps {
type: "performers" | "studios" | "tags" | "movies";
type: "performers" | "studios" | "tags" | "movies" | "galleries";
existingIds?: string[];
ids?: string[];
mode: GQL.BulkUpdateIdMode;
@@ -15,6 +19,39 @@ interface IMultiSetProps {
onSetMode: (mode: GQL.BulkUpdateIdMode) => void;
}
const Select: React.FC<IMultiSetProps> = (props) => {
const { type, disabled } = props;
function onUpdate(items: SelectObject[]) {
props.onUpdate(items.map((i) => i.id));
}
if (type === "galleries") {
return (
<GalleryIDSelect
isDisabled={disabled}
isMulti
isClearable={false}
onSelect={onUpdate}
ids={props.ids ?? []}
// exclude file-based galleries when setting galleries
extraCriteria={excludeFileBasedGalleries}
/>
);
}
return (
<FilterSelect
type={type}
isDisabled={disabled}
isMulti
isClearable={false}
onSelect={onUpdate}
ids={props.ids ?? []}
/>
);
};
export const MultiSet: React.FC<IMultiSetProps> = (props) => {
const intl = useIntl();
const modes = [
@@ -23,10 +60,6 @@ export const MultiSet: React.FC<IMultiSetProps> = (props) => {
GQL.BulkUpdateIdMode.Remove,
];
function onUpdate(items: SelectObject[]) {
props.onUpdate(items.map((i) => i.id));
}
function getModeText(mode: GQL.BulkUpdateIdMode) {
switch (mode) {
case GQL.BulkUpdateIdMode.Set:
@@ -83,14 +116,7 @@ export const MultiSet: React.FC<IMultiSetProps> = (props) => {
<ButtonGroup className="button-group-above">
{modes.map((m) => renderModeButton(m))}
</ButtonGroup>
<FilterSelect
type={props.type}
isDisabled={props.disabled}
isMulti
isClearable={false}
onSelect={onUpdate}
ids={props.ids ?? []}
/>
<Select {...props} />
</div>
);
};

View File

@@ -48,22 +48,16 @@ export function getAggregateStudioId(state: IHasStudio[]) {
return ret;
}
interface IHasPerformers {
performers: IHasID[];
}
export function getAggregatePerformerIds(state: IHasPerformers[]) {
export function getAggregateIds(sortedLists: string[][]) {
let ret: string[] = [];
let first = true;
state.forEach((o) => {
sortedLists.forEach((l) => {
if (first) {
ret = o.performers ? o.performers.map((p) => p.id).sort() : [];
ret = l;
first = false;
} else {
const perfIds = o.performers ? o.performers.map((p) => p.id).sort() : [];
if (!isEqual(ret, perfIds)) {
if (!isEqual(ret, l)) {
ret = [];
}
}
@@ -72,56 +66,30 @@ export function getAggregatePerformerIds(state: IHasPerformers[]) {
return ret;
}
interface IHasTags {
tags: IHasID[];
export function getAggregateGalleryIds(state: { galleries: IHasID[] }[]) {
const sortedLists = state.map((o) => o.galleries.map((oo) => oo.id).sort());
return getAggregateIds(sortedLists);
}
export function getAggregateTagIds(state: IHasTags[]) {
let ret: string[] = [];
let first = true;
state.forEach((o) => {
if (first) {
ret = o.tags ? o.tags.map((t) => t.id).sort() : [];
first = false;
} else {
const tIds = o.tags ? o.tags.map((t) => t.id).sort() : [];
if (!isEqual(ret, tIds)) {
ret = [];
export function getAggregatePerformerIds(state: { performers: IHasID[] }[]) {
const sortedLists = state.map((o) => o.performers.map((oo) => oo.id).sort());
return getAggregateIds(sortedLists);
}
}
});
return ret;
export function getAggregateTagIds(state: { tags: IHasID[] }[]) {
const sortedLists = state.map((o) => o.tags.map((oo) => oo.id).sort());
return getAggregateIds(sortedLists);
}
interface IMovie {
movie: IHasID;
}
interface IHasMovies {
movies: IMovie[];
}
export function getAggregateMovieIds(state: IHasMovies[]) {
let ret: string[] = [];
let first = true;
state.forEach((o) => {
if (first) {
ret = o.movies ? o.movies.map((m) => m.movie.id).sort() : [];
first = false;
} else {
const mIds = o.movies ? o.movies.map((m) => m.movie.id).sort() : [];
if (!isEqual(ret, mIds)) {
ret = [];
}
}
});
return ret;
export function getAggregateMovieIds(state: { movies: IMovie[] }[]) {
const sortedLists = state.map((o) =>
o.movies.map((oo) => oo.movie.id).sort()
);
return getAggregateIds(sortedLists);
}
export function makeBulkUpdateIds(