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, Criterion,
CriterionValue, CriterionValue,
} from "src/models/list-filter/criteria/criterion"; } from "src/models/list-filter/criteria/criterion";
import { PathCriterion } from "src/models/list-filter/criteria/path";
export type Gallery = Pick<GQL.Gallery, "id" | "title"> & { export type Gallery = Pick<GQL.Gallery, "id" | "title"> & {
files: Pick<GQL.GalleryFile, "path">[]; files: Pick<GQL.GalleryFile, "path">[];
@@ -39,14 +40,14 @@ export type Gallery = Pick<GQL.Gallery, "id" | "title"> & {
}; };
type Option = SelectOption<Gallery>; type Option = SelectOption<Gallery>;
const _GallerySelect: React.FC< type ExtraGalleryProps = {
IFilterProps &
IFilterValueProps<Gallery> & {
hoverPlacement?: Placement; hoverPlacement?: Placement;
excludeIds?: string[]; excludeIds?: string[];
} & {
extraCriteria?: Array<Criterion<CriterionValue>>; extraCriteria?: Array<Criterion<CriterionValue>>;
} };
const _GallerySelect: React.FC<
IFilterProps & IFilterValueProps<Gallery> & ExtraGalleryProps
> = (props) => { > = (props) => {
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = React.useContext(ConfigurationContext);
const intl = useIntl(); const intl = useIntl();
@@ -187,9 +188,9 @@ const _GallerySelect: React.FC<
export const GallerySelect = PatchComponent("GallerySelect", _GallerySelect); export const GallerySelect = PatchComponent("GallerySelect", _GallerySelect);
const _GalleryIDSelect: React.FC<IFilterProps & IFilterIDProps<Gallery>> = ( const _GalleryIDSelect: React.FC<
props IFilterProps & IFilterIDProps<Gallery> & ExtraGalleryProps
) => { > = (props) => {
const { ids, onSelect: onSelectValues } = props; const { ids, onSelect: onSelectValues } = props;
const [values, setValues] = useState<Gallery[]>([]); const [values, setValues] = useState<Gallery[]>([]);
@@ -238,3 +239,11 @@ export const GalleryIDSelect = PatchComponent(
"GalleryIDSelect", "GalleryIDSelect",
_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 { MultiSet } from "../Shared/MultiSet";
import { RatingSystem } from "../Shared/Rating/RatingSystem"; import { RatingSystem } from "../Shared/Rating/RatingSystem";
import { import {
getAggregateGalleryIds,
getAggregateInputIDs, getAggregateInputIDs,
getAggregateInputValue, getAggregateInputValue,
getAggregatePerformerIds, getAggregatePerformerIds,
@@ -36,11 +37,19 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add); React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
const [performerIds, setPerformerIds] = useState<string[]>(); const [performerIds, setPerformerIds] = useState<string[]>();
const [existingPerformerIds, setExistingPerformerIds] = useState<string[]>(); const [existingPerformerIds, setExistingPerformerIds] = useState<string[]>();
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>( const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(
GQL.BulkUpdateIdMode.Add GQL.BulkUpdateIdMode.Add
); );
const [tagIds, setTagIds] = useState<string[]>(); const [tagIds, setTagIds] = useState<string[]>();
const [existingTagIds, setExistingTagIds] = 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 [organized, setOrganized] = useState<boolean | undefined>();
const [updateImages] = useBulkImageUpdate(); const [updateImages] = useBulkImageUpdate();
@@ -56,6 +65,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
const aggregateStudioId = getAggregateStudioId(props.selected); const aggregateStudioId = getAggregateStudioId(props.selected);
const aggregatePerformerIds = getAggregatePerformerIds(props.selected); const aggregatePerformerIds = getAggregatePerformerIds(props.selected);
const aggregateTagIds = getAggregateTagIds(props.selected); const aggregateTagIds = getAggregateTagIds(props.selected);
const aggregateGalleryIds = getAggregateGalleryIds(props.selected);
const imageInput: GQL.BulkImageUpdateInput = { const imageInput: GQL.BulkImageUpdateInput = {
ids: props.selected.map((image) => { ids: props.selected.map((image) => {
@@ -72,6 +82,11 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
aggregatePerformerIds aggregatePerformerIds
); );
imageInput.tag_ids = getAggregateInputIDs(tagMode, tagIds, aggregateTagIds); imageInput.tag_ids = getAggregateInputIDs(tagMode, tagIds, aggregateTagIds);
imageInput.gallery_ids = getAggregateInputIDs(
galleryMode,
galleryIds,
aggregateGalleryIds
);
if (organized !== undefined) { if (organized !== undefined) {
imageInput.organized = organized; imageInput.organized = organized;
@@ -107,6 +122,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
let updateStudioID: string | undefined; let updateStudioID: string | undefined;
let updatePerformerIds: string[] = []; let updatePerformerIds: string[] = [];
let updateTagIds: string[] = []; let updateTagIds: string[] = [];
let updateGalleryIds: string[] = [];
let updateOrganized: boolean | undefined; let updateOrganized: boolean | undefined;
let first = true; let first = true;
@@ -117,12 +133,14 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
.map((p) => p.id) .map((p) => p.id)
.sort(); .sort();
const imageTagIDs = (image.tags ?? []).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) { if (first) {
updateRating = imageRating ?? undefined; updateRating = imageRating ?? undefined;
updateStudioID = imageStudioID; updateStudioID = imageStudioID;
updatePerformerIds = imagePerformerIDs; updatePerformerIds = imagePerformerIDs;
updateTagIds = imageTagIDs; updateTagIds = imageTagIDs;
updateGalleryIds = imageGalleryIDs;
updateOrganized = image.organized; updateOrganized = image.organized;
first = false; first = false;
} else { } else {
@@ -138,6 +156,9 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
if (!isEqual(imageTagIDs, updateTagIds)) { if (!isEqual(imageTagIDs, updateTagIds)) {
updateTagIds = []; updateTagIds = [];
} }
if (!isEqual(imageGalleryIDs, updateGalleryIds)) {
updateGalleryIds = [];
}
if (image.organized !== updateOrganized) { if (image.organized !== updateOrganized) {
updateOrganized = undefined; updateOrganized = undefined;
} }
@@ -148,6 +169,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
setStudioId(updateStudioID); setStudioId(updateStudioID);
setExistingPerformerIds(updatePerformerIds); setExistingPerformerIds(updatePerformerIds);
setExistingTagIds(updateTagIds); setExistingTagIds(updateTagIds);
setExistingGalleryIds(updateGalleryIds);
setOrganized(updateOrganized); setOrganized(updateOrganized);
}, [props.selected, performerMode, tagMode]); }, [props.selected, performerMode, tagMode]);
@@ -157,54 +179,6 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
} }
}, [organized, checkboxRef]); }, [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() { function cycleOrganized() {
if (organized) { if (organized) {
setOrganized(undefined); setOrganized(undefined);
@@ -271,14 +245,45 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
<Form.Label> <Form.Label>
<FormattedMessage id="performers" /> <FormattedMessage id="performers" />
</Form.Label> </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>
<Form.Group controlId="tags"> <Form.Group controlId="tags">
<Form.Label> <Form.Label>
<FormattedMessage id="tags" /> <FormattedMessage id="tags" />
</Form.Label> </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>
<Form.Group controlId="organized"> <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 { Tag, TagSelect } from "src/components/Tags/TagSelect";
import { Studio, StudioSelect } from "src/components/Studios/StudioSelect"; import { Studio, StudioSelect } from "src/components/Studios/StudioSelect";
import { galleryTitle } from "src/core/galleries"; import { galleryTitle } from "src/core/galleries";
import { Gallery, GallerySelect } from "src/components/Galleries/GallerySelect"; import {
import { PathCriterion } from "src/models/list-filter/criteria/path"; Gallery,
GallerySelect,
excludeFileBasedGalleries,
} from "src/components/Galleries/GallerySelect";
interface IProps { interface IProps {
image: GQL.ImageDataFragment; image: GQL.ImageDataFragment;
@@ -34,14 +37,6 @@ interface IProps {
onDelete: () => void; onDelete: () => void;
} }
function getExcludeFilebaseGalleriesFilter() {
const ret = new PathCriterion();
ret.modifier = GQL.CriterionModifier.IsNull;
return ret;
}
const excludeFileBasedGalleries = [getExcludeFilebaseGalleriesFilter()];
export const ImageEditPanel: React.FC<IProps> = ({ export const ImageEditPanel: React.FC<IProps> = ({
image, image,
isVisible, isVisible,

View File

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

View File

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