mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 04:44:37 +03:00
Improve bulk performer editing (#2467)
* Cleanup Edit Performers dialog * Add bulk text inputs to edit performers dialog * Make bulk update code more generic * Add remaining performer fields
This commit is contained in:
@@ -2,8 +2,21 @@ fragment SlimPerformerData on Performer {
|
|||||||
id
|
id
|
||||||
name
|
name
|
||||||
gender
|
gender
|
||||||
|
url
|
||||||
|
twitter
|
||||||
|
instagram
|
||||||
image_path
|
image_path
|
||||||
favorite
|
favorite
|
||||||
|
country
|
||||||
|
birthdate
|
||||||
|
ethnicity
|
||||||
|
hair_color
|
||||||
|
eye_color
|
||||||
|
height
|
||||||
|
fake_tits
|
||||||
|
career_length
|
||||||
|
tattoos
|
||||||
|
piercings
|
||||||
tags {
|
tags {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
@@ -13,4 +26,6 @@ fragment SlimPerformerData on Performer {
|
|||||||
stash_id
|
stash_id
|
||||||
}
|
}
|
||||||
rating
|
rating
|
||||||
|
death_date
|
||||||
|
weight
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
* Add python location in System Settings for script scrapers and plugins. ([#2409](https://github.com/stashapp/stash/pull/2409))
|
* Add python location in System Settings for script scrapers and plugins. ([#2409](https://github.com/stashapp/stash/pull/2409))
|
||||||
|
|
||||||
### 🎨 Improvements
|
### 🎨 Improvements
|
||||||
|
* Added support for bulk editing most performer fields. ([#2467](https://github.com/stashapp/stash/pull/2467))
|
||||||
* Changed video player to videojs. ([#2100](https://github.com/stashapp/stash/pull/2100))
|
* Changed video player to videojs. ([#2100](https://github.com/stashapp/stash/pull/2100))
|
||||||
* Maintain lightbox settings and add lightbox settings to Interface settings page. ([#2406](https://github.com/stashapp/stash/pull/2406))
|
* Maintain lightbox settings and add lightbox settings to Interface settings page. ([#2406](https://github.com/stashapp/stash/pull/2406))
|
||||||
* Image lightbox now transitions to next/previous image when scrolling in pan-Y mode. ([#2403](https://github.com/stashapp/stash/pull/2403))
|
* Image lightbox now transitions to next/previous image when scrolling in pan-Y mode. ([#2403](https://github.com/stashapp/stash/pull/2403))
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Form, Col, Row } from "react-bootstrap";
|
import { Form, Col, Row } from "react-bootstrap";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import _ from "lodash";
|
|
||||||
import { useBulkPerformerUpdate } from "src/core/StashService";
|
import { useBulkPerformerUpdate } from "src/core/StashService";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { Modal } from "src/components/Shared";
|
import { Modal } from "src/components/Shared";
|
||||||
@@ -10,39 +9,63 @@ import { FormUtils } from "src/utils";
|
|||||||
import MultiSet from "../Shared/MultiSet";
|
import MultiSet from "../Shared/MultiSet";
|
||||||
import { RatingStars } from "../Scenes/SceneDetails/RatingStars";
|
import { RatingStars } from "../Scenes/SceneDetails/RatingStars";
|
||||||
import {
|
import {
|
||||||
getAggregateInputIDs,
|
|
||||||
getAggregateInputValue,
|
getAggregateInputValue,
|
||||||
getAggregateRating,
|
getAggregateState,
|
||||||
getAggregateTagIds,
|
getAggregateStateObject,
|
||||||
} from "src/utils/bulkUpdate";
|
} from "src/utils/bulkUpdate";
|
||||||
import { genderStrings, stringToGender } from "src/utils/gender";
|
import {
|
||||||
|
genderStrings,
|
||||||
|
genderToString,
|
||||||
|
stringToGender,
|
||||||
|
} from "src/utils/gender";
|
||||||
|
import { IndeterminateCheckbox } from "../Shared/IndeterminateCheckbox";
|
||||||
|
import { BulkUpdateTextInput } from "../Shared/BulkUpdateTextInput";
|
||||||
|
|
||||||
interface IListOperationProps {
|
interface IListOperationProps {
|
||||||
selected: GQL.SlimPerformerDataFragment[];
|
selected: GQL.SlimPerformerDataFragment[];
|
||||||
onClose: (applied: boolean) => void;
|
onClose: (applied: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const performerFields = [
|
||||||
|
"favorite",
|
||||||
|
"url",
|
||||||
|
"instagram",
|
||||||
|
"twitter",
|
||||||
|
"rating",
|
||||||
|
"gender",
|
||||||
|
"birthdate",
|
||||||
|
"death_date",
|
||||||
|
"career_length",
|
||||||
|
"country",
|
||||||
|
"ethnicity",
|
||||||
|
"eye_color",
|
||||||
|
"height",
|
||||||
|
// "weight",
|
||||||
|
"measurements",
|
||||||
|
"fake_tits",
|
||||||
|
"hair_color",
|
||||||
|
"tattoos",
|
||||||
|
"piercings",
|
||||||
|
];
|
||||||
|
|
||||||
export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
||||||
props: IListOperationProps
|
props: IListOperationProps
|
||||||
) => {
|
) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const [rating, setRating] = useState<number>();
|
const [tagIds, setTagIds] = useState<GQL.BulkUpdateIds>({
|
||||||
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(
|
mode: GQL.BulkUpdateIdMode.Add,
|
||||||
GQL.BulkUpdateIdMode.Add
|
});
|
||||||
);
|
|
||||||
const [tagIds, setTagIds] = useState<string[]>();
|
|
||||||
const [existingTagIds, setExistingTagIds] = useState<string[]>();
|
const [existingTagIds, setExistingTagIds] = useState<string[]>();
|
||||||
const [favorite, setFavorite] = useState<boolean | undefined>();
|
const [
|
||||||
const [ethnicity, setEthnicity] = useState<string | undefined>();
|
aggregateState,
|
||||||
const [country, setCountry] = useState<string | undefined>();
|
setAggregateState,
|
||||||
const [eyeColor, setEyeColor] = useState<string | undefined>();
|
] = useState<GQL.BulkPerformerUpdateInput>({});
|
||||||
const [fakeTits, setFakeTits] = useState<string | undefined>();
|
// weight needs conversion to/from number
|
||||||
const [careerLength, setCareerLength] = useState<string | undefined>();
|
const [weight, setWeight] = useState<string | undefined>();
|
||||||
const [tattoos, setTattoos] = useState<string | undefined>();
|
const [updateInput, setUpdateInput] = useState<GQL.BulkPerformerUpdateInput>(
|
||||||
const [piercings, setPiercings] = useState<string | undefined>();
|
{}
|
||||||
const [hairColor, setHairColor] = useState<string | undefined>();
|
);
|
||||||
const [gender, setGender] = useState<GQL.GenderEnum | undefined>();
|
|
||||||
const genderOptions = [""].concat(genderStrings);
|
const genderOptions = [""].concat(genderStrings);
|
||||||
|
|
||||||
const [updatePerformers] = useBulkPerformerUpdate(getPerformerInput());
|
const [updatePerformers] = useBulkPerformerUpdate(getPerformerInput());
|
||||||
@@ -50,37 +73,36 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||||||
// Network state
|
// Network state
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
|
|
||||||
const checkboxRef = React.createRef<HTMLInputElement>();
|
function setUpdateField(input: Partial<GQL.BulkPerformerUpdateInput>) {
|
||||||
|
setUpdateInput({ ...updateInput, ...input });
|
||||||
|
}
|
||||||
|
|
||||||
function getPerformerInput(): GQL.BulkPerformerUpdateInput {
|
function getPerformerInput(): GQL.BulkPerformerUpdateInput {
|
||||||
// need to determine what we are actually setting on each performer
|
|
||||||
const aggregateTagIds = getAggregateTagIds(props.selected);
|
|
||||||
const aggregateRating = getAggregateRating(props.selected);
|
|
||||||
|
|
||||||
const performerInput: GQL.BulkPerformerUpdateInput = {
|
const performerInput: GQL.BulkPerformerUpdateInput = {
|
||||||
ids: props.selected.map((performer) => {
|
ids: props.selected.map((performer) => {
|
||||||
return performer.id;
|
return performer.id;
|
||||||
}),
|
}),
|
||||||
|
...updateInput,
|
||||||
|
tag_ids: tagIds,
|
||||||
};
|
};
|
||||||
|
|
||||||
performerInput.rating = getAggregateInputValue(rating, aggregateRating);
|
// we don't have unset functionality for the rating star control
|
||||||
|
// so need to determine if we are setting a rating or not
|
||||||
performerInput.tag_ids = getAggregateInputIDs(
|
performerInput.rating = getAggregateInputValue(
|
||||||
tagMode,
|
updateInput.rating,
|
||||||
tagIds,
|
aggregateState.rating
|
||||||
aggregateTagIds
|
|
||||||
);
|
);
|
||||||
|
|
||||||
performerInput.favorite = favorite;
|
// gender dropdown doesn't have unset functionality
|
||||||
performerInput.ethnicity = ethnicity;
|
// so need to determine what we are setting
|
||||||
performerInput.country = country;
|
performerInput.gender = getAggregateInputValue(
|
||||||
performerInput.eye_color = eyeColor;
|
updateInput.gender,
|
||||||
performerInput.fake_tits = fakeTits;
|
aggregateState.gender
|
||||||
performerInput.career_length = careerLength;
|
);
|
||||||
performerInput.tattoos = tattoos;
|
|
||||||
performerInput.piercings = piercings;
|
if (weight !== undefined) {
|
||||||
performerInput.hair_color = hairColor;
|
performerInput.weight = parseFloat(weight);
|
||||||
performerInput.gender = gender;
|
}
|
||||||
|
|
||||||
return performerInput;
|
return performerInput;
|
||||||
}
|
}
|
||||||
@@ -107,74 +129,39 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const updateState: GQL.BulkPerformerUpdateInput = {};
|
||||||
|
|
||||||
const state = props.selected;
|
const state = props.selected;
|
||||||
let updateTagIds: string[] = [];
|
let updateTagIds: string[] = [];
|
||||||
let updateFavorite: boolean | undefined;
|
let updateWeight: string | undefined | null = undefined;
|
||||||
let updateRating: number | undefined;
|
|
||||||
let updateGender: GQL.GenderEnum | undefined;
|
|
||||||
let first = true;
|
let first = true;
|
||||||
|
|
||||||
state.forEach((performer: GQL.SlimPerformerDataFragment) => {
|
state.forEach((performer: GQL.SlimPerformerDataFragment) => {
|
||||||
const performerTagIDs = (performer.tags ?? []).map((p) => p.id).sort();
|
getAggregateStateObject(updateState, performer, performerFields, first);
|
||||||
const performerRating = performer.rating;
|
|
||||||
|
|
||||||
if (first) {
|
const performerTagIDs = (performer.tags ?? []).map((p) => p.id).sort();
|
||||||
updateTagIds = performerTagIDs;
|
|
||||||
first = false;
|
updateTagIds =
|
||||||
updateFavorite = performer.favorite;
|
getAggregateState(updateTagIds, performerTagIDs, first) ?? [];
|
||||||
updateRating = performerRating ?? undefined;
|
|
||||||
updateGender = performer.gender ?? undefined;
|
const thisWeight =
|
||||||
} else {
|
performer.weight !== undefined && performer.weight !== null
|
||||||
if (!_.isEqual(performerTagIDs, updateTagIds)) {
|
? performer.weight.toString()
|
||||||
updateTagIds = [];
|
: performer.weight;
|
||||||
}
|
updateWeight = getAggregateState(updateWeight, thisWeight, first);
|
||||||
if (performer.favorite !== updateFavorite) {
|
|
||||||
updateFavorite = undefined;
|
first = false;
|
||||||
}
|
|
||||||
if (performerRating !== updateRating) {
|
|
||||||
updateRating = undefined;
|
|
||||||
}
|
|
||||||
if (performer.gender !== updateGender) {
|
|
||||||
updateGender = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setExistingTagIds(updateTagIds);
|
setExistingTagIds(updateTagIds);
|
||||||
setFavorite(updateFavorite);
|
setWeight(updateWeight);
|
||||||
setRating(updateRating);
|
setAggregateState(updateState);
|
||||||
setGender(updateGender);
|
setUpdateInput(updateState);
|
||||||
|
}, [props.selected]);
|
||||||
// these fields are not part of SlimPerformerDataFragment
|
|
||||||
setEthnicity(undefined);
|
|
||||||
setCountry(undefined);
|
|
||||||
setEyeColor(undefined);
|
|
||||||
setFakeTits(undefined);
|
|
||||||
setCareerLength(undefined);
|
|
||||||
setTattoos(undefined);
|
|
||||||
setPiercings(undefined);
|
|
||||||
setHairColor(undefined);
|
|
||||||
}, [props.selected, tagMode]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (checkboxRef.current) {
|
|
||||||
checkboxRef.current.indeterminate = favorite === undefined;
|
|
||||||
}
|
|
||||||
}, [favorite, checkboxRef]);
|
|
||||||
|
|
||||||
function cycleFavorite() {
|
|
||||||
if (favorite) {
|
|
||||||
setFavorite(undefined);
|
|
||||||
} else if (favorite === undefined) {
|
|
||||||
setFavorite(false);
|
|
||||||
} else {
|
|
||||||
setFavorite(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTextField(
|
function renderTextField(
|
||||||
name: string,
|
name: string,
|
||||||
value: string | undefined,
|
value: string | undefined | null,
|
||||||
setter: (newValue: string | undefined) => void
|
setter: (newValue: string | undefined) => void
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
@@ -182,12 +169,10 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||||||
<Form.Label>
|
<Form.Label>
|
||||||
<FormattedMessage id={name} />
|
<FormattedMessage id={name} />
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Form.Control
|
<BulkUpdateTextInput
|
||||||
className="input-control"
|
value={value === null ? "" : value ?? undefined}
|
||||||
type="text"
|
valueChanged={(newValue) => setter(newValue)}
|
||||||
value={value}
|
unsetDisabled={props.selected.length < 2}
|
||||||
onChange={(event) => setter(event.currentTarget.value)}
|
|
||||||
placeholder={intl.formatMessage({ id: name })}
|
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
);
|
);
|
||||||
@@ -219,20 +204,18 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||||||
})}
|
})}
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<RatingStars
|
<RatingStars
|
||||||
value={rating}
|
value={updateInput.rating ?? undefined}
|
||||||
onSetRating={(value) => setRating(value)}
|
onSetRating={(value) => setUpdateField({ rating: value })}
|
||||||
disabled={isUpdating}
|
disabled={isUpdating}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Group controlId="favorite">
|
<Form.Group controlId="favorite">
|
||||||
<Form.Check
|
<IndeterminateCheckbox
|
||||||
type="checkbox"
|
setChecked={(checked) => setUpdateField({ favorite: checked })}
|
||||||
label="Favorite"
|
checked={updateInput.favorite ?? undefined}
|
||||||
checked={favorite}
|
label={intl.formatMessage({ id: "favourite" })}
|
||||||
ref={checkboxRef}
|
|
||||||
onChange={() => cycleFavorite()}
|
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
@@ -243,8 +226,11 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||||||
<Form.Control
|
<Form.Control
|
||||||
as="select"
|
as="select"
|
||||||
className="input-control"
|
className="input-control"
|
||||||
|
value={genderToString(updateInput.gender ?? undefined)}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
setGender(stringToGender(event.currentTarget.value))
|
setUpdateField({
|
||||||
|
gender: stringToGender(event.currentTarget.value),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{genderOptions.map((opt) => (
|
{genderOptions.map((opt) => (
|
||||||
@@ -255,14 +241,52 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||||||
</Form.Control>
|
</Form.Control>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
{renderTextField("country", country, setCountry)}
|
{renderTextField("birthdate", updateInput.birthdate, (v) =>
|
||||||
{renderTextField("ethnicity", ethnicity, setEthnicity)}
|
setUpdateField({ birthdate: v })
|
||||||
{renderTextField("hair_color", hairColor, setHairColor)}
|
)}
|
||||||
{renderTextField("eye_color", eyeColor, setEyeColor)}
|
{renderTextField("death_date", updateInput.death_date, (v) =>
|
||||||
{renderTextField("fake_tits", fakeTits, setFakeTits)}
|
setUpdateField({ death_date: v })
|
||||||
{renderTextField("tattoos", tattoos, setTattoos)}
|
)}
|
||||||
{renderTextField("piercings", piercings, setPiercings)}
|
{renderTextField("country", updateInput.country, (v) =>
|
||||||
{renderTextField("career_length", careerLength, setCareerLength)}
|
setUpdateField({ country: v })
|
||||||
|
)}
|
||||||
|
{renderTextField("ethnicity", updateInput.ethnicity, (v) =>
|
||||||
|
setUpdateField({ ethnicity: v })
|
||||||
|
)}
|
||||||
|
{renderTextField("hair_color", updateInput.hair_color, (v) =>
|
||||||
|
setUpdateField({ hair_color: v })
|
||||||
|
)}
|
||||||
|
{renderTextField("eye_color", updateInput.eye_color, (v) =>
|
||||||
|
setUpdateField({ eye_color: v })
|
||||||
|
)}
|
||||||
|
{renderTextField("height", updateInput.height, (v) =>
|
||||||
|
setUpdateField({ height: v })
|
||||||
|
)}
|
||||||
|
{renderTextField("weight", weight, (v) => setWeight(v))}
|
||||||
|
{renderTextField("measurements", updateInput.measurements, (v) =>
|
||||||
|
setUpdateField({ measurements: v })
|
||||||
|
)}
|
||||||
|
{renderTextField("fake_tits", updateInput.fake_tits, (v) =>
|
||||||
|
setUpdateField({ fake_tits: v })
|
||||||
|
)}
|
||||||
|
{renderTextField("tattoos", updateInput.tattoos, (v) =>
|
||||||
|
setUpdateField({ tattoos: v })
|
||||||
|
)}
|
||||||
|
{renderTextField("piercings", updateInput.piercings, (v) =>
|
||||||
|
setUpdateField({ piercings: v })
|
||||||
|
)}
|
||||||
|
{renderTextField("career_length", updateInput.career_length, (v) =>
|
||||||
|
setUpdateField({ career_length: v })
|
||||||
|
)}
|
||||||
|
{renderTextField("url", updateInput.url, (v) =>
|
||||||
|
setUpdateField({ url: v })
|
||||||
|
)}
|
||||||
|
{renderTextField("twitter", updateInput.twitter, (v) =>
|
||||||
|
setUpdateField({ twitter: v })
|
||||||
|
)}
|
||||||
|
{renderTextField("instagram", updateInput.instagram, (v) =>
|
||||||
|
setUpdateField({ instagram: v })
|
||||||
|
)}
|
||||||
|
|
||||||
<Form.Group controlId="tags">
|
<Form.Group controlId="tags">
|
||||||
<Form.Label>
|
<Form.Label>
|
||||||
@@ -271,11 +295,11 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||||||
<MultiSet
|
<MultiSet
|
||||||
type="tags"
|
type="tags"
|
||||||
disabled={isUpdating}
|
disabled={isUpdating}
|
||||||
onUpdate={(itemIDs) => setTagIds(itemIDs)}
|
onUpdate={(itemIDs) => setTagIds({ ...tagIds, ids: itemIDs })}
|
||||||
onSetMode={(newMode) => setTagMode(newMode)}
|
onSetMode={(newMode) => setTagIds({ ...tagIds, mode: newMode })}
|
||||||
existingIds={existingTagIds ?? []}
|
existingIds={existingTagIds ?? []}
|
||||||
ids={tagIds ?? []}
|
ids={tagIds.ids ?? []}
|
||||||
mode={tagMode}
|
mode={tagIds.mode}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
45
ui/v2.5/src/components/Shared/BulkUpdateTextInput.tsx
Normal file
45
ui/v2.5/src/components/Shared/BulkUpdateTextInput.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Button, Form, FormControlProps, InputGroup } from "react-bootstrap";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
import { Icon } from ".";
|
||||||
|
|
||||||
|
interface IBulkUpdateTextInputProps extends FormControlProps {
|
||||||
|
valueChanged: (value: string | undefined) => void;
|
||||||
|
unsetDisabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BulkUpdateTextInput: React.FC<IBulkUpdateTextInputProps> = ({
|
||||||
|
valueChanged,
|
||||||
|
unsetDisabled,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const unsetClassName = props.value === undefined ? "unset" : "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputGroup className={`bulk-update-text-input ${unsetClassName}`}>
|
||||||
|
<Form.Control
|
||||||
|
{...props}
|
||||||
|
className="input-control"
|
||||||
|
type="text"
|
||||||
|
value={props.value ?? ""}
|
||||||
|
placeholder={
|
||||||
|
props.value === undefined
|
||||||
|
? `<${intl.formatMessage({ id: "existing_value" })}>`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onChange={(event) => valueChanged(event.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
{!unsetDisabled ? (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => valueChanged(undefined)}
|
||||||
|
title={intl.formatMessage({ id: "actions.unset" })}
|
||||||
|
>
|
||||||
|
<Icon icon="ban" />
|
||||||
|
</Button>
|
||||||
|
) : undefined}
|
||||||
|
</InputGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -20,7 +20,7 @@ interface IIcon {
|
|||||||
const Icon: React.FC<IIcon> = ({ icon, className, color, size }) => (
|
const Icon: React.FC<IIcon> = ({ icon, className, color, size }) => (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={icon}
|
icon={icon}
|
||||||
className={`fa-icon ${className}`}
|
className={`fa-icon ${className ?? ""}`}
|
||||||
color={color}
|
color={color}
|
||||||
size={size}
|
size={size}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const IndeterminateCheckbox: React.FC<IIndeterminateCheckbox> = ({
|
|||||||
checked === undefined ? indeterminateClassname : ""
|
checked === undefined ? indeterminateClassname : ""
|
||||||
}`}
|
}`}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
checked={checked}
|
checked={checked ?? false}
|
||||||
onChange={() => setChecked(cycleState())}
|
onChange={() => setChecked(cycleState())}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ const MultiSet: React.FunctionComponent<IMultiSetProps> = (
|
|||||||
function renderModeButton(mode: GQL.BulkUpdateIdMode) {
|
function renderModeButton(mode: GQL.BulkUpdateIdMode) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
key={mode}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
active={props.mode === mode}
|
active={props.mode === mode}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -271,3 +271,30 @@ button.collapse-button.btn-primary:not(:disabled):not(.disabled):active {
|
|||||||
.string-list-input .input-group {
|
.string-list-input .input-group {
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bulk-update-text-input {
|
||||||
|
button {
|
||||||
|
background-color: $secondary;
|
||||||
|
color: $text-muted;
|
||||||
|
font-size: $btn-font-size-sm;
|
||||||
|
margin: $btn-padding-y $btn-padding-x;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
z-index: 4;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active,
|
||||||
|
&:not(:disabled):not(.disabled):active,
|
||||||
|
&:not(:disabled):not(.disabled):active:focus {
|
||||||
|
background-color: $secondary;
|
||||||
|
border-color: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.unset button {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -101,6 +101,7 @@
|
|||||||
},
|
},
|
||||||
"temp_disable": "Disable temporarily…",
|
"temp_disable": "Disable temporarily…",
|
||||||
"temp_enable": "Enable temporarily…",
|
"temp_enable": "Enable temporarily…",
|
||||||
|
"unset": "Unset",
|
||||||
"use_default": "Use default",
|
"use_default": "Use default",
|
||||||
"view_random": "View Random",
|
"view_random": "View Random",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
@@ -584,6 +585,7 @@
|
|||||||
"delete_object_overflow": "…and {count} other {count, plural, one {{singularEntity}} other {{pluralEntity}}}.",
|
"delete_object_overflow": "…and {count} other {count, plural, one {{singularEntity}} other {{pluralEntity}}}.",
|
||||||
"delete_object_title": "Delete {count, plural, one {{singularEntity}} other {{pluralEntity}}}",
|
"delete_object_title": "Delete {count, plural, one {{singularEntity}} other {{pluralEntity}}}",
|
||||||
"edit_entity_title": "Edit {count, plural, one {{singularEntity}} other {{pluralEntity}}}",
|
"edit_entity_title": "Edit {count, plural, one {{singularEntity}} other {{pluralEntity}}}",
|
||||||
|
"existing_value": "existing value",
|
||||||
"export_include_related_objects": "Include related objects in export",
|
"export_include_related_objects": "Include related objects in export",
|
||||||
"export_title": "Export",
|
"export_title": "Export",
|
||||||
"lightbox": {
|
"lightbox": {
|
||||||
@@ -701,6 +703,7 @@
|
|||||||
"warmth": "Warmth"
|
"warmth": "Warmth"
|
||||||
},
|
},
|
||||||
"ethnicity": "Ethnicity",
|
"ethnicity": "Ethnicity",
|
||||||
|
"existing_value": "existing value",
|
||||||
"eye_color": "Eye Colour",
|
"eye_color": "Eye Colour",
|
||||||
"fake_tits": "Fake Tits",
|
"fake_tits": "Fake Tits",
|
||||||
"false": "False",
|
"false": "False",
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export function getAggregateMovieIds(state: IHasMovies[]) {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeBulkUpdateIds(
|
export function makeBulkUpdateIds(
|
||||||
ids: string[],
|
ids: string[],
|
||||||
mode: GQL.BulkUpdateIdMode
|
mode: GQL.BulkUpdateIdMode
|
||||||
): GQL.BulkUpdateIds {
|
): GQL.BulkUpdateIds {
|
||||||
@@ -152,6 +152,7 @@ export function getAggregateInputValue<V>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO - remove - this is incorrect
|
||||||
export function getAggregateInputIDs(
|
export function getAggregateInputIDs(
|
||||||
mode: GQL.BulkUpdateIdMode,
|
mode: GQL.BulkUpdateIdMode,
|
||||||
inputIds: string[] | undefined,
|
inputIds: string[] | undefined,
|
||||||
@@ -173,3 +174,46 @@ export function getAggregateInputIDs(
|
|||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAggregateState<T>(
|
||||||
|
currentValue: T,
|
||||||
|
newValue: T,
|
||||||
|
first: boolean
|
||||||
|
) {
|
||||||
|
if (!first && !_.isEqual(currentValue, newValue)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
function setProperty<T, K extends keyof T>(obj: T, key: K, value: any) {
|
||||||
|
obj[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProperty<T, K extends keyof T>(obj: T, key: K) {
|
||||||
|
return obj[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAggregateStateObject<O, I>(
|
||||||
|
output: O,
|
||||||
|
input: I,
|
||||||
|
fields: string[],
|
||||||
|
first: boolean
|
||||||
|
) {
|
||||||
|
fields.forEach((key) => {
|
||||||
|
const outputKey = key as keyof O;
|
||||||
|
const inputKey = key as keyof I;
|
||||||
|
|
||||||
|
const currentValue = getProperty(output, outputKey);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const performerValue = getProperty(input, inputKey) as any;
|
||||||
|
|
||||||
|
setProperty(
|
||||||
|
output,
|
||||||
|
outputKey,
|
||||||
|
getAggregateState(currentValue, performerValue, first)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user