mirror of
https://github.com/stashapp/stash.git
synced 2026-03-04 04:03:21 +01:00
FR: Custom Fields Frontend (#6601)
* Add "custom-field-" prefix to custom field detail item ids --------- Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
parent
cf04e854d6
commit
01d351c85d
33 changed files with 434 additions and 69 deletions
|
|
@ -39,6 +39,8 @@ fragment GalleryData on Gallery {
|
|||
scenes {
|
||||
...SlimSceneData
|
||||
}
|
||||
|
||||
custom_fields
|
||||
}
|
||||
|
||||
fragment SelectGalleryData on Gallery {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ fragment GroupData on Group {
|
|||
id
|
||||
title
|
||||
}
|
||||
|
||||
custom_fields
|
||||
}
|
||||
|
||||
# Lightweight fragment for list views - excludes expensive recursive counts
|
||||
|
|
|
|||
|
|
@ -37,4 +37,6 @@ fragment ImageData on Image {
|
|||
visual_files {
|
||||
...VisualFileData
|
||||
}
|
||||
|
||||
custom_fields
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,8 @@ fragment SceneData on Scene {
|
|||
mime_type
|
||||
label
|
||||
}
|
||||
|
||||
custom_fields
|
||||
}
|
||||
|
||||
fragment SelectSceneData on Scene {
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ fragment StudioData on Studio {
|
|||
...SlimTagData
|
||||
}
|
||||
o_counter
|
||||
custom_fields
|
||||
}
|
||||
|
||||
fragment SelectStudioData on Studio {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,14 @@ query FindScene($id: ID!, $checksum: String) {
|
|||
}
|
||||
}
|
||||
|
||||
query FindFullScenes($ids: [Int!]) {
|
||||
findScenes(scene_ids: $ids) {
|
||||
scenes {
|
||||
...SceneData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query FindSceneMarkerTags($id: ID!) {
|
||||
sceneMarkerTags(scene_id: $id) {
|
||||
tag {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { TagLink } from "src/components/Shared/TagLink";
|
|||
import { PerformerCard } from "src/components/Performers/PerformerCard";
|
||||
import { sortPerformers } from "src/core/performers";
|
||||
import { PhotographerLink } from "src/components/Shared/Link";
|
||||
import { CustomFields } from "src/components/Shared/CustomFields";
|
||||
|
||||
interface IGalleryDetailProps {
|
||||
gallery: GQL.GalleryDataFragment;
|
||||
|
|
@ -108,6 +109,7 @@ export const GalleryDetailPanel: React.FC<IGalleryDetailProps> = ({
|
|||
{renderDetails()}
|
||||
{renderTags()}
|
||||
{renderPerformers()}
|
||||
<CustomFields values={gallery.custom_fields} fullWidth />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,11 @@ import { Studio, StudioSelect } from "src/components/Studios/StudioSelect";
|
|||
import { Scene, SceneSelect } from "src/components/Scenes/SceneSelect";
|
||||
import { useTagsEdit } from "src/hooks/tagsEdit";
|
||||
import { ScraperMenu } from "src/components/Shared/ScraperMenu";
|
||||
import {
|
||||
CustomFieldsInput,
|
||||
formatCustomFieldInput,
|
||||
} from "src/components/Shared/CustomFields";
|
||||
import { cloneDeep } from "@apollo/client/utilities";
|
||||
|
||||
interface IProps {
|
||||
gallery: Partial<GQL.GalleryDataFragment>;
|
||||
|
|
@ -76,6 +81,7 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||
tag_ids: yup.array(yup.string().required()).defined(),
|
||||
scene_ids: yup.array(yup.string().required()).defined(),
|
||||
details: yup.string().ensure(),
|
||||
custom_fields: yup.object().required().defined(),
|
||||
});
|
||||
|
||||
const initialValues = {
|
||||
|
|
@ -89,15 +95,26 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||
tag_ids: (gallery?.tags ?? []).map((t) => t.id),
|
||||
scene_ids: (gallery?.scenes ?? []).map((s) => s.id),
|
||||
details: gallery?.details ?? "",
|
||||
custom_fields: cloneDeep(gallery?.custom_fields ?? {}),
|
||||
};
|
||||
|
||||
type InputValues = yup.InferType<typeof schema>;
|
||||
|
||||
const [customFieldsError, setCustomFieldsError] = useState<string>();
|
||||
|
||||
function submit(values: InputValues) {
|
||||
const input = {
|
||||
...schema.cast(values),
|
||||
custom_fields: formatCustomFieldInput(isNew, values.custom_fields),
|
||||
};
|
||||
onSave(input);
|
||||
}
|
||||
|
||||
const formik = useFormik<InputValues>({
|
||||
initialValues,
|
||||
enableReinitialize: true,
|
||||
validate: yupFormikValidate(schema),
|
||||
onSubmit: (values) => onSave(schema.cast(values)),
|
||||
onSubmit: submit,
|
||||
});
|
||||
|
||||
const { tags, updateTagsStateFromScraper, tagsControl } = useTagsEdit(
|
||||
|
|
@ -189,7 +206,10 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||
}
|
||||
|
||||
async function onSaveAndNewClick() {
|
||||
const input = schema.cast(formik.values);
|
||||
const input = {
|
||||
...schema.cast(formik.values),
|
||||
custom_fields: formatCustomFieldInput(isNew, formik.values.custom_fields),
|
||||
};
|
||||
onSave(input, true);
|
||||
}
|
||||
|
||||
|
|
@ -455,7 +475,9 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||
id="gallery-save-split-button"
|
||||
className="edit-button"
|
||||
variant="primary"
|
||||
disabled={!isEqual(formik.errors, {})}
|
||||
disabled={
|
||||
!isEqual(formik.errors, {}) || customFieldsError !== undefined
|
||||
}
|
||||
title={intl.formatMessage({ id: "actions.save" })}
|
||||
onClick={() => formik.submitForm()}
|
||||
>
|
||||
|
|
@ -468,7 +490,9 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||
className="edit-button"
|
||||
variant="primary"
|
||||
disabled={
|
||||
(!isNew && !formik.dirty) || !isEqual(formik.errors, {})
|
||||
(!isNew && !formik.dirty) ||
|
||||
!isEqual(formik.errors, {}) ||
|
||||
customFieldsError !== undefined
|
||||
}
|
||||
onClick={() => formik.submitForm()}
|
||||
>
|
||||
|
|
@ -523,6 +547,13 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||
</Form.Label>
|
||||
{cover}
|
||||
</Form.Group>
|
||||
|
||||
<CustomFieldsInput
|
||||
values={formik.values.custom_fields}
|
||||
onChange={(v) => formik.setFieldValue("custom_fields", v)}
|
||||
error={customFieldsError}
|
||||
setError={(e) => setCustomFieldsError(e)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -208,6 +208,20 @@ $galleryTabWidth: 450px;
|
|||
.form-group[data-field="urls"] .string-list-input input.form-control {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(xl) {
|
||||
.custom-fields-input {
|
||||
.custom-fields-field {
|
||||
flex: 0 0 25%;
|
||||
max-width: 25%;
|
||||
}
|
||||
|
||||
.custom-fields-value {
|
||||
flex: 0 0 75%;
|
||||
max-width: 75%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gallery-cover {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { DetailItem } from "src/components/Shared/DetailItem";
|
|||
import { Link } from "react-router-dom";
|
||||
import { DirectorLink } from "src/components/Shared/Link";
|
||||
import { GroupLink, TagLink } from "src/components/Shared/TagLink";
|
||||
import { CustomFields } from "src/components/Shared/CustomFields";
|
||||
|
||||
interface IGroupDescription {
|
||||
group: GQL.SlimGroupDataFragment;
|
||||
|
|
@ -101,6 +102,7 @@ export const GroupDetailsPanel: React.FC<IGroupDetailsPanel> = ({
|
|||
fullWidth={fullWidth}
|
||||
/>
|
||||
)}
|
||||
<CustomFields values={group.custom_fields} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -28,6 +28,11 @@ import { Studio, StudioSelect } from "src/components/Studios/StudioSelect";
|
|||
import { useTagsEdit } from "src/hooks/tagsEdit";
|
||||
import { Group } from "src/components/Groups/GroupSelect";
|
||||
import { RelatedGroupTable, IRelatedGroupEntry } from "./RelatedGroupTable";
|
||||
import {
|
||||
CustomFieldsInput,
|
||||
formatCustomFieldInput,
|
||||
} from "src/components/Shared/CustomFields";
|
||||
import { cloneDeep } from "@apollo/client/utilities";
|
||||
|
||||
interface IGroupEditPanel {
|
||||
group: Partial<GQL.GroupDataFragment>;
|
||||
|
|
@ -84,6 +89,7 @@ export const GroupEditPanel: React.FC<IGroupEditPanel> = ({
|
|||
synopsis: yup.string().ensure(),
|
||||
front_image: yup.string().nullable().optional(),
|
||||
back_image: yup.string().nullable().optional(),
|
||||
custom_fields: yup.object().required().defined(),
|
||||
});
|
||||
|
||||
const initialValues = {
|
||||
|
|
@ -99,15 +105,26 @@ export const GroupEditPanel: React.FC<IGroupEditPanel> = ({
|
|||
director: group?.director ?? "",
|
||||
urls: group?.urls ?? [],
|
||||
synopsis: group?.synopsis ?? "",
|
||||
custom_fields: cloneDeep(group?.custom_fields ?? {}),
|
||||
};
|
||||
|
||||
type InputValues = yup.InferType<typeof schema>;
|
||||
|
||||
const [customFieldsError, setCustomFieldsError] = useState<string>();
|
||||
|
||||
function submit(values: InputValues) {
|
||||
const input = {
|
||||
...schema.cast(values),
|
||||
custom_fields: formatCustomFieldInput(isNew, values.custom_fields),
|
||||
};
|
||||
onSave(input);
|
||||
}
|
||||
|
||||
const formik = useFormik<InputValues>({
|
||||
initialValues,
|
||||
enableReinitialize: true,
|
||||
validate: yupFormikValidate(schema),
|
||||
onSubmit: (values) => onSave(schema.cast(values)),
|
||||
onSubmit: submit,
|
||||
});
|
||||
|
||||
const { tags, updateTagsStateFromScraper, tagsControl } = useTagsEdit(
|
||||
|
|
@ -220,7 +237,10 @@ export const GroupEditPanel: React.FC<IGroupEditPanel> = ({
|
|||
}
|
||||
|
||||
async function onSaveAndNewClick() {
|
||||
const input = schema.cast(formik.values);
|
||||
const input = {
|
||||
...schema.cast(formik.values),
|
||||
custom_fields: formatCustomFieldInput(isNew, formik.values.custom_fields),
|
||||
};
|
||||
onSave(input, true);
|
||||
}
|
||||
|
||||
|
|
@ -458,6 +478,13 @@ export const GroupEditPanel: React.FC<IGroupEditPanel> = ({
|
|||
{renderURLListField("urls", onScrapeGroupURL, urlScrapable)}
|
||||
{renderInputField("synopsis", "textarea")}
|
||||
{renderTagsField()}
|
||||
|
||||
<CustomFieldsInput
|
||||
values={formik.values.custom_fields}
|
||||
onChange={(v) => formik.setFieldValue("custom_fields", v)}
|
||||
error={customFieldsError}
|
||||
setError={(e) => setCustomFieldsError(e)}
|
||||
/>
|
||||
</Form>
|
||||
|
||||
<DetailsEditNavbar
|
||||
|
|
@ -468,7 +495,11 @@ export const GroupEditPanel: React.FC<IGroupEditPanel> = ({
|
|||
onToggleEdit={onCancel}
|
||||
onSave={formik.handleSubmit}
|
||||
onSaveAndNew={isNew ? onSaveAndNewClick : undefined}
|
||||
saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
||||
saveDisabled={
|
||||
(!isNew && !formik.dirty) ||
|
||||
!isEqual(formik.errors, {}) ||
|
||||
customFieldsError !== undefined
|
||||
}
|
||||
onImageChange={onFrontImageChange}
|
||||
onImageChangeURL={onFrontImageLoad}
|
||||
onClearImage={() => onFrontImageLoad(null)}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { sortPerformers } from "src/core/performers";
|
|||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { PhotographerLink } from "src/components/Shared/Link";
|
||||
import { PatchComponent } from "../../../patch";
|
||||
import { CustomFields } from "src/components/Shared/CustomFields";
|
||||
interface IImageDetailProps {
|
||||
image: GQL.ImageDataFragment;
|
||||
}
|
||||
|
|
@ -132,6 +133,7 @@ export const ImageDetailPanel: React.FC<IImageDetailProps> = PatchComponent(
|
|||
{renderDetails()}
|
||||
{renderTags()}
|
||||
{renderPerformers()}
|
||||
<CustomFields values={props.image.custom_fields} fullWidth />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -35,6 +35,11 @@ import {
|
|||
} from "src/components/Galleries/GallerySelect";
|
||||
import { useTagsEdit } from "src/hooks/tagsEdit";
|
||||
import { ScraperMenu } from "src/components/Shared/ScraperMenu";
|
||||
import {
|
||||
CustomFieldsInput,
|
||||
formatCustomFieldInput,
|
||||
} from "src/components/Shared/CustomFields";
|
||||
import { cloneDeep } from "@apollo/client/utilities";
|
||||
|
||||
interface IProps {
|
||||
image: GQL.ImageDataFragment;
|
||||
|
|
@ -86,6 +91,7 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||
studio_id: yup.string().required().nullable(),
|
||||
performer_ids: yup.array(yup.string().required()).defined(),
|
||||
tag_ids: yup.array(yup.string().required()).defined(),
|
||||
custom_fields: yup.object().required().defined(),
|
||||
});
|
||||
|
||||
const initialValues = {
|
||||
|
|
@ -99,15 +105,26 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||
studio_id: image.studio?.id ?? null,
|
||||
performer_ids: (image.performers ?? []).map((p) => p.id),
|
||||
tag_ids: (image.tags ?? []).map((t) => t.id),
|
||||
custom_fields: cloneDeep(image.custom_fields ?? {}),
|
||||
};
|
||||
|
||||
type InputValues = yup.InferType<typeof schema>;
|
||||
|
||||
const [customFieldsError, setCustomFieldsError] = useState<string>();
|
||||
|
||||
function submit(values: InputValues) {
|
||||
const input = {
|
||||
...schema.cast(values),
|
||||
custom_fields: formatCustomFieldInput(isNew, values.custom_fields),
|
||||
};
|
||||
onSave(input);
|
||||
}
|
||||
|
||||
const formik = useFormik<InputValues>({
|
||||
initialValues,
|
||||
enableReinitialize: true,
|
||||
validate: yupFormikValidate(schema),
|
||||
onSubmit: (values) => onSave(schema.cast(values)),
|
||||
onSubmit: submit,
|
||||
});
|
||||
|
||||
const { tags, updateTagsStateFromScraper, tagsControl } = useTagsEdit(
|
||||
|
|
@ -444,7 +461,9 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||
className="edit-button"
|
||||
variant="primary"
|
||||
disabled={
|
||||
(!isNew && !formik.dirty) || !isEqual(formik.errors, {})
|
||||
(!isNew && !formik.dirty) ||
|
||||
!isEqual(formik.errors, {}) ||
|
||||
customFieldsError !== undefined
|
||||
}
|
||||
onClick={() => formik.submitForm()}
|
||||
>
|
||||
|
|
@ -492,6 +511,13 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||
</Col>
|
||||
<Col lg={5} xl={12}>
|
||||
{renderDetailsField()}
|
||||
|
||||
<CustomFieldsInput
|
||||
values={formik.values.custom_fields}
|
||||
onChange={(v) => formik.setFieldValue("custom_fields", v)}
|
||||
error={customFieldsError}
|
||||
setError={(e) => setCustomFieldsError(e)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -179,6 +179,20 @@ $imageTabWidth: 450px;
|
|||
.form-group[data-field="urls"] .string-list-input input.form-control {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(xl) {
|
||||
.custom-fields-input {
|
||||
.custom-fields-field {
|
||||
flex: 0 0 25%;
|
||||
max-width: 25%;
|
||||
}
|
||||
|
||||
.custom-fields-value {
|
||||
flex: 0 0 75%;
|
||||
max-width: 75%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-file-card.card {
|
||||
|
|
|
|||
|
|
@ -48,7 +48,10 @@ import {
|
|||
yupUniqueStringList,
|
||||
} from "src/utils/yup";
|
||||
import { useTagsEdit } from "src/hooks/tagsEdit";
|
||||
import { CustomFieldsInput } from "src/components/Shared/CustomFields";
|
||||
import {
|
||||
CustomFieldsInput,
|
||||
formatCustomFieldInput,
|
||||
} from "src/components/Shared/CustomFields";
|
||||
import { cloneDeep } from "@apollo/client/utilities";
|
||||
|
||||
const isScraper = (
|
||||
|
|
@ -67,16 +70,6 @@ interface IPerformerDetails {
|
|||
setEncodingImage: (loading: boolean) => void;
|
||||
}
|
||||
|
||||
function customFieldInput(isNew: boolean, input: {}) {
|
||||
if (isNew) {
|
||||
return input;
|
||||
} else {
|
||||
return {
|
||||
full: input,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
performer,
|
||||
isVisible,
|
||||
|
|
@ -173,7 +166,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
function submit(values: InputValues) {
|
||||
const input = {
|
||||
...schema.cast(values),
|
||||
custom_fields: customFieldInput(isNew, values.custom_fields),
|
||||
custom_fields: formatCustomFieldInput(isNew, values.custom_fields),
|
||||
};
|
||||
onSave(input);
|
||||
}
|
||||
|
|
@ -368,7 +361,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
const { values } = formik;
|
||||
const input = {
|
||||
...schema.cast(values),
|
||||
custom_fields: customFieldInput(isNew, values.custom_fields),
|
||||
custom_fields: formatCustomFieldInput(isNew, values.custom_fields),
|
||||
};
|
||||
onSave(input, true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,11 +82,6 @@
|
|||
font-weight: 700;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.custom-fields .detail-item-title,
|
||||
.custom-fields .detail-item-value {
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
}
|
||||
/* stylelint-enable selector-class-pattern */
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { TagLink } from "src/components/Shared/TagLink";
|
|||
import { PerformerCard } from "src/components/Performers/PerformerCard";
|
||||
import { sortPerformers } from "src/core/performers";
|
||||
import { DirectorLink } from "src/components/Shared/Link";
|
||||
import { CustomFields } from "src/components/Shared/CustomFields";
|
||||
|
||||
interface ISceneDetailProps {
|
||||
scene: GQL.SceneDataFragment;
|
||||
|
|
@ -103,6 +104,7 @@ export const SceneDetailPanel: React.FC<ISceneDetailProps> = (props) => {
|
|||
{renderDetails()}
|
||||
{renderTags()}
|
||||
{renderPerformers()}
|
||||
<CustomFields values={props.scene.custom_fields} fullWidth />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,11 @@ import { Group } from "src/components/Groups/GroupSelect";
|
|||
import { useTagsEdit } from "src/hooks/tagsEdit";
|
||||
import { ScraperMenu } from "src/components/Shared/ScraperMenu";
|
||||
import StashBoxIDSearchModal from "src/components/Shared/StashBoxIDSearchModal";
|
||||
import {
|
||||
CustomFieldsInput,
|
||||
formatCustomFieldInput,
|
||||
} from "src/components/Shared/CustomFields";
|
||||
import { cloneDeep } from "@apollo/client/utilities";
|
||||
|
||||
const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog"));
|
||||
const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal"));
|
||||
|
|
@ -140,6 +145,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
stash_ids: yup.mixed<GQL.StashIdInput[]>().defined(),
|
||||
details: yup.string().ensure(),
|
||||
cover_image: yup.string().nullable().optional(),
|
||||
custom_fields: yup.object().required().defined(),
|
||||
});
|
||||
|
||||
const initialValues = useMemo(
|
||||
|
|
@ -159,17 +165,28 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
stash_ids: getStashIDs(scene.stash_ids),
|
||||
details: scene.details ?? "",
|
||||
cover_image: initialCoverImage,
|
||||
custom_fields: cloneDeep(scene.custom_fields ?? {}),
|
||||
}),
|
||||
[scene, initialCoverImage]
|
||||
);
|
||||
|
||||
type InputValues = yup.InferType<typeof schema>;
|
||||
|
||||
const [customFieldsError, setCustomFieldsError] = useState<string>();
|
||||
|
||||
function submit(values: InputValues) {
|
||||
const input = {
|
||||
...schema.cast(values),
|
||||
custom_fields: formatCustomFieldInput(isNew, values.custom_fields),
|
||||
};
|
||||
onSave(input);
|
||||
}
|
||||
|
||||
const formik = useFormik<InputValues>({
|
||||
initialValues,
|
||||
enableReinitialize: true,
|
||||
validate: yupFormikValidate(schema),
|
||||
onSubmit: (values) => onSave(schema.cast(values)),
|
||||
onSubmit: submit,
|
||||
});
|
||||
|
||||
const { tags, updateTagsStateFromScraper, tagsControl } = useTagsEdit(
|
||||
|
|
@ -288,7 +305,10 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
}
|
||||
|
||||
async function onSaveAndNewClick() {
|
||||
const input = schema.cast(formik.values);
|
||||
const input = {
|
||||
...schema.cast(formik.values),
|
||||
custom_fields: formatCustomFieldInput(isNew, formik.values.custom_fields),
|
||||
};
|
||||
onSave(input, true);
|
||||
}
|
||||
|
||||
|
|
@ -759,7 +779,9 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
id="scene-save-split-button"
|
||||
className="edit-button"
|
||||
variant="primary"
|
||||
disabled={!isEqual(formik.errors, {})}
|
||||
disabled={
|
||||
!isEqual(formik.errors, {}) || customFieldsError !== undefined
|
||||
}
|
||||
title={intl.formatMessage({ id: "actions.save" })}
|
||||
onClick={() => formik.submitForm()}
|
||||
>
|
||||
|
|
@ -772,7 +794,9 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
className="edit-button"
|
||||
variant="primary"
|
||||
disabled={
|
||||
(!isNew && !formik.dirty) || !isEqual(formik.errors, {})
|
||||
(!isNew && !formik.dirty) ||
|
||||
!isEqual(formik.errors, {}) ||
|
||||
customFieldsError !== undefined
|
||||
}
|
||||
onClick={() => formik.submitForm()}
|
||||
>
|
||||
|
|
@ -863,6 +887,13 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
onReset={scene.id ? onResetCover : undefined}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<CustomFieldsInput
|
||||
values={formik.values.custom_fields}
|
||||
onChange={(v) => formik.setFieldValue("custom_fields", v)}
|
||||
error={customFieldsError}
|
||||
setError={(e) => setCustomFieldsError(e)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -7,12 +7,16 @@ import { StringListSelect, GallerySelect } from "../Shared/Select";
|
|||
import * as FormUtils from "src/utils/form";
|
||||
import ImageUtils from "src/utils/image";
|
||||
import TextUtils from "src/utils/text";
|
||||
import { mutateSceneMerge, queryFindScenesByID } from "src/core/StashService";
|
||||
import {
|
||||
mutateSceneMerge,
|
||||
queryFindFullScenesByID,
|
||||
} from "src/core/StashService";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { faExchangeAlt, faSignInAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
ScrapeDialogRow,
|
||||
ScrapedCustomFieldRows,
|
||||
ScrapedImageRow,
|
||||
ScrapedInputGroupRow,
|
||||
ScrapedStringListRow,
|
||||
|
|
@ -24,6 +28,7 @@ import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
|||
import { ModalComponent } from "../Shared/Modal";
|
||||
import { IHasStoredID, sortStoredIdObjects } from "src/utils/data";
|
||||
import {
|
||||
CustomFieldScrapeResults,
|
||||
ObjectListScrapeResult,
|
||||
ScrapeResult,
|
||||
ZeroableScrapeResult,
|
||||
|
|
@ -52,8 +57,8 @@ type MergeOptions = {
|
|||
};
|
||||
|
||||
interface ISceneMergeDetailsProps {
|
||||
sources: GQL.SlimSceneDataFragment[];
|
||||
dest: GQL.SlimSceneDataFragment;
|
||||
sources: GQL.SceneDataFragment[];
|
||||
dest: GQL.SceneDataFragment;
|
||||
onClose: (options?: MergeOptions) => void;
|
||||
}
|
||||
|
||||
|
|
@ -173,6 +178,10 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||
new ScrapeResult<string>(dest.paths.screenshot)
|
||||
);
|
||||
|
||||
const [customFields, setCustomFields] = useState<CustomFieldScrapeResults>(
|
||||
new Map()
|
||||
);
|
||||
|
||||
// calculate the values for everything
|
||||
// uses the first set value for single value fields, and combines all
|
||||
useEffect(() => {
|
||||
|
|
@ -309,28 +318,64 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||
)
|
||||
);
|
||||
|
||||
const customFieldNames = new Set<string>(
|
||||
Object.keys(dest.custom_fields ?? {})
|
||||
);
|
||||
|
||||
for (const s of sources) {
|
||||
for (const n of Object.keys(s.custom_fields ?? {})) {
|
||||
customFieldNames.add(n);
|
||||
}
|
||||
}
|
||||
|
||||
setCustomFields(
|
||||
new Map(
|
||||
Array.from(customFieldNames)
|
||||
.sort()
|
||||
.map((field) => {
|
||||
return [
|
||||
field,
|
||||
new ScrapeResult(
|
||||
dest.custom_fields?.[field],
|
||||
sources.find((s) => s.custom_fields?.[field])?.custom_fields?.[
|
||||
field
|
||||
],
|
||||
dest.custom_fields?.[field] === undefined
|
||||
),
|
||||
];
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
loadImages();
|
||||
}, [sources, dest]);
|
||||
|
||||
const hasCustomFieldValues = useMemo(() => {
|
||||
return hasScrapedValues(Array.from(customFields.values()));
|
||||
}, [customFields]);
|
||||
|
||||
// ensure this is updated if fields are changed
|
||||
const hasValues = useMemo(() => {
|
||||
return hasScrapedValues([
|
||||
title,
|
||||
code,
|
||||
url,
|
||||
date,
|
||||
rating,
|
||||
oCounter,
|
||||
galleries,
|
||||
studio,
|
||||
performers,
|
||||
groups,
|
||||
tags,
|
||||
details,
|
||||
organized,
|
||||
stashIDs,
|
||||
image,
|
||||
]);
|
||||
return (
|
||||
hasCustomFieldValues ||
|
||||
hasScrapedValues([
|
||||
title,
|
||||
code,
|
||||
url,
|
||||
date,
|
||||
rating,
|
||||
oCounter,
|
||||
galleries,
|
||||
studio,
|
||||
performers,
|
||||
groups,
|
||||
tags,
|
||||
details,
|
||||
organized,
|
||||
stashIDs,
|
||||
image,
|
||||
])
|
||||
);
|
||||
}, [
|
||||
title,
|
||||
code,
|
||||
|
|
@ -347,6 +392,7 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||
organized,
|
||||
stashIDs,
|
||||
image,
|
||||
hasCustomFieldValues,
|
||||
]);
|
||||
|
||||
function renderScrapeRows() {
|
||||
|
|
@ -566,6 +612,12 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||
result={image}
|
||||
onChange={(value) => setImage(value)}
|
||||
/>
|
||||
{hasCustomFieldValues && (
|
||||
<ScrapedCustomFieldRows
|
||||
results={customFields}
|
||||
onChange={(newCustomFields) => setCustomFields(newCustomFields)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -606,6 +658,13 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||
organized: organized.getNewValue(),
|
||||
stash_ids: stashIDs.getNewValue(),
|
||||
cover_image: coverImage,
|
||||
custom_fields: {
|
||||
partial: Object.fromEntries(
|
||||
Array.from(customFields.entries()).flatMap(([field, v]) =>
|
||||
v.useNewValue ? [[field, v.getNewValue()]] : []
|
||||
)
|
||||
),
|
||||
},
|
||||
},
|
||||
includeViewHistory: playCount.getNewValue() !== undefined,
|
||||
includeOHistory: oCounter.getNewValue() !== undefined,
|
||||
|
|
@ -655,10 +714,10 @@ export const SceneMergeModal: React.FC<ISceneMergeModalProps> = ({
|
|||
const [sourceScenes, setSourceScenes] = useState<Scene[]>([]);
|
||||
const [destScene, setDestScene] = useState<Scene[]>([]);
|
||||
|
||||
const [loadedSources, setLoadedSources] = useState<
|
||||
GQL.SlimSceneDataFragment[]
|
||||
>([]);
|
||||
const [loadedDest, setLoadedDest] = useState<GQL.SlimSceneDataFragment>();
|
||||
const [loadedSources, setLoadedSources] = useState<GQL.SceneDataFragment[]>(
|
||||
[]
|
||||
);
|
||||
const [loadedDest, setLoadedDest] = useState<GQL.SceneDataFragment>();
|
||||
|
||||
const [running, setRunning] = useState(false);
|
||||
const [secondStep, setSecondStep] = useState(false);
|
||||
|
|
@ -684,7 +743,7 @@ export const SceneMergeModal: React.FC<ISceneMergeModalProps> = ({
|
|||
async function loadScenes() {
|
||||
const sceneIDs = sourceScenes.map((s) => parseInt(s.id));
|
||||
sceneIDs.push(parseInt(destScene[0].id));
|
||||
const query = await queryFindScenesByID(sceneIDs);
|
||||
const query = await queryFindFullScenesByID(sceneIDs);
|
||||
const { scenes: loadedScenes } = query.data.findScenes;
|
||||
|
||||
setLoadedDest(loadedScenes.find((s) => s.id === destScene[0].id));
|
||||
|
|
|
|||
|
|
@ -562,6 +562,20 @@ input[type="range"].blue-slider {
|
|||
.form-group[data-field="urls"] .string-list-input input.form-control {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(xl) {
|
||||
.custom-fields-input {
|
||||
.custom-fields-field {
|
||||
flex: 0 0 25%;
|
||||
max-width: 25%;
|
||||
}
|
||||
|
||||
.custom-fields-value {
|
||||
flex: 0 0 75%;
|
||||
max-width: 75%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scene-markers-panel {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export type CustomFieldMap = {
|
|||
|
||||
interface ICustomFields {
|
||||
values: CustomFieldMap;
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
function convertValue(value: unknown): string {
|
||||
|
|
@ -41,7 +42,7 @@ const CustomField: React.FC<{ field: string; value: unknown }> = ({
|
|||
const valueStr = convertValue(value);
|
||||
|
||||
// replace spaces with hyphen characters for css id
|
||||
const id = field.toLowerCase().replace(/ /g, "-");
|
||||
const id = `custom-field-${field.toLowerCase().replace(/ /g, "-")}`;
|
||||
|
||||
return (
|
||||
<DetailItem
|
||||
|
|
@ -57,7 +58,7 @@ const CustomField: React.FC<{ field: string; value: unknown }> = ({
|
|||
|
||||
export const CustomFields: React.FC<ICustomFields> = PatchComponent(
|
||||
"CustomFields",
|
||||
({ values }) => {
|
||||
({ values, fullWidth }) => {
|
||||
const intl = useIntl();
|
||||
if (Object.keys(values).length === 0) {
|
||||
return null;
|
||||
|
|
@ -65,7 +66,7 @@ export const CustomFields: React.FC<ICustomFields> = PatchComponent(
|
|||
|
||||
return (
|
||||
// according to linter rule CSS classes shouldn't use underscores
|
||||
<div className="custom-fields">
|
||||
<div className={cx("custom-fields", { "full-width": fullWidth })}>
|
||||
<CollapseButton
|
||||
text={intl.formatMessage({ id: "custom_fields.title" })}
|
||||
>
|
||||
|
|
@ -125,7 +126,7 @@ const CustomFieldInput: React.FC<{
|
|||
<Row
|
||||
className={cx("custom-fields-row", { "custom-fields-new": isNew })}
|
||||
>
|
||||
<Col sm={3} xl={2} className="custom-fields-field">
|
||||
<Col className="custom-fields-field">
|
||||
{isNew ? (
|
||||
<>
|
||||
<Form.Control
|
||||
|
|
@ -146,7 +147,7 @@ const CustomFieldInput: React.FC<{
|
|||
<Form.Label title={currentField}>{currentField}</Form.Label>
|
||||
)}
|
||||
</Col>
|
||||
<Col sm={9} xl={7}>
|
||||
<Col className="custom-fields-value">
|
||||
<InputGroup>
|
||||
<Form.Control
|
||||
ref={valueRef}
|
||||
|
|
@ -189,6 +190,16 @@ interface ICustomFieldsInput {
|
|||
setError: (error?: string) => void;
|
||||
}
|
||||
|
||||
export function formatCustomFieldInput(isNew: boolean, input: {}) {
|
||||
if (isNew) {
|
||||
return input;
|
||||
} else {
|
||||
return {
|
||||
full: input,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const CustomFieldsInput: React.FC<ICustomFieldsInput> = PatchComponent(
|
||||
"CustomFieldsInput",
|
||||
({ values, error, onChange, setError }) => {
|
||||
|
|
@ -282,10 +293,10 @@ export const CustomFieldsInput: React.FC<ICustomFieldsInput> = PatchComponent(
|
|||
<Row>
|
||||
<Col xl={12}>
|
||||
<Row className="custom-fields-input-header">
|
||||
<Form.Label column sm={3} xl={2}>
|
||||
<Form.Label column className="custom-fields-field">
|
||||
<FormattedMessage id="custom_fields.field" />
|
||||
</Form.Label>
|
||||
<Form.Label column sm={9} xl={7}>
|
||||
<Form.Label column className="custom-fields-value">
|
||||
<FormattedMessage id="custom_fields.value" />
|
||||
</Form.Label>
|
||||
</Row>
|
||||
|
|
|
|||
|
|
@ -795,6 +795,11 @@ button.btn.favorite-button {
|
|||
.detail-item {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.detail-item-title,
|
||||
.detail-item-value {
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-fields .detail-item .detail-item-title {
|
||||
|
|
@ -816,6 +821,36 @@ button.btn.favorite-button {
|
|||
font-weight: 700;
|
||||
}
|
||||
|
||||
.custom-fields-input {
|
||||
.custom-fields-field {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
flex: 0 0 25%;
|
||||
max-width: 25%;
|
||||
}
|
||||
@include media-breakpoint-up(xl) {
|
||||
flex: 0 0 16.667%;
|
||||
max-width: 16.667%;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-fields-value {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
flex: 0 0 75%;
|
||||
max-width: 75%;
|
||||
}
|
||||
@include media-breakpoint-up(xl) {
|
||||
flex: 0 0 58.33%;
|
||||
max-width: 58.33%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-fields-row {
|
||||
align-items: center;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import * as GQL from "src/core/generated-graphql";
|
|||
import { DetailItem } from "src/components/Shared/DetailItem";
|
||||
import { StashIDPill } from "src/components/Shared/StashID";
|
||||
import { PatchComponent } from "src/patch";
|
||||
import { CustomFields } from "src/components/Shared/CustomFields";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
interface IStudioDetailsPanel {
|
||||
|
|
@ -87,6 +88,7 @@ export const StudioDetailsPanel: React.FC<IStudioDetailsPanel> = PatchComponent(
|
|||
value={renderStashIDs()}
|
||||
fullWidth={fullWidth}
|
||||
/>
|
||||
<CustomFields values={studio.custom_fields} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,11 @@ import { Studio, StudioSelect } from "../StudioSelect";
|
|||
import { useTagsEdit } from "src/hooks/tagsEdit";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
import StashBoxIDSearchModal from "src/components/Shared/StashBoxIDSearchModal";
|
||||
import {
|
||||
CustomFieldsInput,
|
||||
formatCustomFieldInput,
|
||||
} from "src/components/Shared/CustomFields";
|
||||
import { cloneDeep } from "@apollo/client/utilities";
|
||||
|
||||
interface IStudioEditPanel {
|
||||
studio: Partial<GQL.StudioDataFragment>;
|
||||
|
|
@ -63,6 +68,7 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||
ignore_auto_tag: yup.boolean().defined(),
|
||||
stash_ids: yup.mixed<GQL.StashIdInput[]>().defined(),
|
||||
image: yup.string().nullable().optional(),
|
||||
custom_fields: yup.object().required().defined(),
|
||||
});
|
||||
|
||||
const initialValues = {
|
||||
|
|
@ -75,15 +81,26 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||
tag_ids: (studio.tags ?? []).map((t) => t.id),
|
||||
ignore_auto_tag: studio.ignore_auto_tag ?? false,
|
||||
stash_ids: getStashIDs(studio.stash_ids),
|
||||
custom_fields: cloneDeep(studio.custom_fields ?? {}),
|
||||
};
|
||||
|
||||
type InputValues = yup.InferType<typeof schema>;
|
||||
|
||||
const [customFieldsError, setCustomFieldsError] = useState<string>();
|
||||
|
||||
function submit(values: InputValues) {
|
||||
const input = {
|
||||
...schema.cast(values),
|
||||
custom_fields: formatCustomFieldInput(isNew, values.custom_fields),
|
||||
};
|
||||
onSave(input);
|
||||
}
|
||||
|
||||
const formik = useFormik<InputValues>({
|
||||
initialValues,
|
||||
enableReinitialize: true,
|
||||
validate: yupFormikValidate(schema),
|
||||
onSubmit: (values) => onSave(schema.cast(values)),
|
||||
onSubmit: submit,
|
||||
});
|
||||
|
||||
const { tagsControl } = useTagsEdit(studio.tags, (ids) =>
|
||||
|
|
@ -144,7 +161,10 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||
}
|
||||
|
||||
async function onSaveAndNewClick() {
|
||||
const input = schema.cast(formik.values);
|
||||
const input = {
|
||||
...schema.cast(formik.values),
|
||||
custom_fields: formatCustomFieldInput(isNew, formik.values.custom_fields),
|
||||
};
|
||||
onSave(input, true);
|
||||
}
|
||||
|
||||
|
|
@ -242,6 +262,14 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||
<Icon icon={faPlus} />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<CustomFieldsInput
|
||||
values={formik.values.custom_fields}
|
||||
onChange={(v) => formik.setFieldValue("custom_fields", v)}
|
||||
error={customFieldsError}
|
||||
setError={(e) => setCustomFieldsError(e)}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
{renderInputField("ignore_auto_tag", "checkbox")}
|
||||
</Form>
|
||||
|
|
@ -254,7 +282,11 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||
onToggleEdit={onCancel}
|
||||
onSave={formik.handleSubmit}
|
||||
onSaveAndNew={isNew ? onSaveAndNewClick : undefined}
|
||||
saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
||||
saveDisabled={
|
||||
(!isNew && !formik.dirty) ||
|
||||
!isEqual(formik.errors, {}) ||
|
||||
customFieldsError !== undefined
|
||||
}
|
||||
onImageChange={onImageChange}
|
||||
onImageChangeURL={onImageLoad}
|
||||
onClearImage={() => onImageLoad(null)}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { TagLink } from "src/components/Shared/TagLink";
|
|||
import { DetailItem } from "src/components/Shared/DetailItem";
|
||||
import { StashIDPill } from "src/components/Shared/StashID";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { CustomFields } from "src/components/Shared/CustomFields";
|
||||
|
||||
interface ITagDetails {
|
||||
tag: GQL.TagDataFragment;
|
||||
|
|
@ -90,6 +91,7 @@ export const TagDetailsPanel: React.FC<ITagDetails> = ({ tag, fullWidth }) => {
|
|||
value={renderStashIDs()}
|
||||
fullWidth={fullWidth}
|
||||
/>
|
||||
<CustomFields values={tag.custom_fields} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,6 +20,11 @@ import { addUpdateStashID, getStashIDs } from "src/utils/stashIds";
|
|||
import { Tag, TagSelect } from "../TagSelect";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
import StashBoxIDSearchModal from "src/components/Shared/StashBoxIDSearchModal";
|
||||
import {
|
||||
CustomFieldsInput,
|
||||
formatCustomFieldInput,
|
||||
} from "src/components/Shared/CustomFields";
|
||||
import { cloneDeep } from "@apollo/client/utilities";
|
||||
|
||||
interface ITagEditPanel {
|
||||
tag: Partial<GQL.TagDataFragment>;
|
||||
|
|
@ -63,6 +68,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||
ignore_auto_tag: yup.boolean().defined(),
|
||||
stash_ids: yup.mixed<GQL.StashIdInput[]>().defined(),
|
||||
image: yup.string().nullable().optional(),
|
||||
custom_fields: yup.object().required().defined(),
|
||||
});
|
||||
|
||||
const initialValues = {
|
||||
|
|
@ -74,15 +80,26 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||
child_ids: (tag?.children ?? []).map((t) => t.id),
|
||||
ignore_auto_tag: tag?.ignore_auto_tag ?? false,
|
||||
stash_ids: getStashIDs(tag?.stash_ids),
|
||||
custom_fields: cloneDeep(tag?.custom_fields ?? {}),
|
||||
};
|
||||
|
||||
type InputValues = yup.InferType<typeof schema>;
|
||||
|
||||
const [customFieldsError, setCustomFieldsError] = useState<string>();
|
||||
|
||||
function submit(values: InputValues) {
|
||||
const input = {
|
||||
...schema.cast(values),
|
||||
custom_fields: formatCustomFieldInput(isNew, values.custom_fields),
|
||||
};
|
||||
onSave(input);
|
||||
}
|
||||
|
||||
const formik = useFormik<InputValues>({
|
||||
initialValues,
|
||||
enableReinitialize: true,
|
||||
validate: yupFormikValidate(schema),
|
||||
onSubmit: (values) => onSave(schema.cast(values)),
|
||||
onSubmit: submit,
|
||||
});
|
||||
|
||||
function onSetParentTags(items: Tag[]) {
|
||||
|
|
@ -134,7 +151,10 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||
}
|
||||
|
||||
async function onSaveAndNewClick() {
|
||||
const input = schema.cast(formik.values);
|
||||
const input = {
|
||||
...schema.cast(formik.values),
|
||||
custom_fields: formatCustomFieldInput(isNew, formik.values.custom_fields),
|
||||
};
|
||||
onSave(input, true);
|
||||
}
|
||||
|
||||
|
|
@ -266,6 +286,14 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||
<Icon icon={faPlus} />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<CustomFieldsInput
|
||||
values={formik.values.custom_fields}
|
||||
onChange={(v) => formik.setFieldValue("custom_fields", v)}
|
||||
error={customFieldsError}
|
||||
setError={(e) => setCustomFieldsError(e)}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
{renderInputField("ignore_auto_tag", "checkbox")}
|
||||
</Form>
|
||||
|
|
@ -279,7 +307,9 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||
onSave={formik.handleSubmit}
|
||||
onSaveAndNew={isNew ? onSaveAndNewClick : undefined}
|
||||
saveDisabled={
|
||||
(!isNew && !formik.dirty) || !isEqual(formik.errors, {})
|
||||
(!isNew && !formik.dirty) ||
|
||||
!isEqual(formik.errors, {}) ||
|
||||
customFieldsError !== undefined
|
||||
}
|
||||
onImageChange={onImageChange}
|
||||
onImageChangeURL={onImageLoad}
|
||||
|
|
|
|||
|
|
@ -166,6 +166,14 @@ export const queryFindScenesByID = (sceneIDs: number[]) =>
|
|||
},
|
||||
});
|
||||
|
||||
export const queryFindFullScenesByID = (sceneIDs: number[]) =>
|
||||
client.query<GQL.FindFullScenesQuery>({
|
||||
query: GQL.FindFullScenesDocument,
|
||||
variables: {
|
||||
ids: sceneIDs,
|
||||
},
|
||||
});
|
||||
|
||||
export const queryFindScenesForSelect = (filter: ListFilterModel) =>
|
||||
client.query<GQL.FindScenesForSelectQuery>({
|
||||
query: GQL.FindScenesForSelectDocument,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { ListFilterOptions, MediaSortByOptions } from "./filter-options";
|
|||
import { DisplayMode } from "./types";
|
||||
import { RatingCriterionOption } from "./criteria/rating";
|
||||
import { PathCriterionOption } from "./criteria/path";
|
||||
import { CustomFieldsCriterionOption } from "./criteria/custom-fields";
|
||||
|
||||
const defaultSortBy = "path";
|
||||
|
||||
|
|
@ -71,6 +72,7 @@ const criterionOptions = [
|
|||
createDateCriterionOption("date"),
|
||||
createMandatoryTimestampCriterionOption("created_at"),
|
||||
createMandatoryTimestampCriterionOption("updated_at"),
|
||||
CustomFieldsCriterionOption,
|
||||
];
|
||||
|
||||
export const GalleryListFilterOptions = new ListFilterOptions(
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
ContainingGroupsCriterionOption,
|
||||
SubGroupsCriterionOption,
|
||||
} from "./criteria/groups";
|
||||
import { CustomFieldsCriterionOption } from "./criteria/custom-fields";
|
||||
|
||||
const defaultSortBy = "name";
|
||||
|
||||
|
|
@ -66,6 +67,7 @@ const criterionOptions = [
|
|||
createMandatoryNumberCriterionOption("scene_count"),
|
||||
createMandatoryTimestampCriterionOption("created_at"),
|
||||
createMandatoryTimestampCriterionOption("updated_at"),
|
||||
CustomFieldsCriterionOption,
|
||||
];
|
||||
|
||||
export const GroupListFilterOptions = new ListFilterOptions(
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { ListFilterOptions, MediaSortByOptions } from "./filter-options";
|
|||
import { DisplayMode } from "./types";
|
||||
import { GalleriesCriterionOption } from "./criteria/galleries";
|
||||
import { PhashCriterionOption } from "./criteria/phash";
|
||||
import { CustomFieldsCriterionOption } from "./criteria/custom-fields";
|
||||
|
||||
const defaultSortBy = "path";
|
||||
|
||||
|
|
@ -73,6 +74,7 @@ const criterionOptions = [
|
|||
createMandatoryNumberCriterionOption("file_count"),
|
||||
createMandatoryTimestampCriterionOption("created_at"),
|
||||
createMandatoryTimestampCriterionOption("updated_at"),
|
||||
CustomFieldsCriterionOption,
|
||||
];
|
||||
export const ImageListFilterOptions = new ListFilterOptions(
|
||||
defaultSortBy,
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import { StashIDCriterionOption } from "./criteria/stash-ids";
|
|||
import { RatingCriterionOption } from "./criteria/rating";
|
||||
import { PathCriterionOption } from "./criteria/path";
|
||||
import { OrientationCriterionOption } from "./criteria/orientation";
|
||||
import { CustomFieldsCriterionOption } from "./criteria/custom-fields";
|
||||
|
||||
const defaultSortBy = "date";
|
||||
const sortByOptions = [
|
||||
|
|
@ -141,6 +142,7 @@ const criterionOptions = [
|
|||
createDateCriterionOption("date"),
|
||||
createMandatoryTimestampCriterionOption("created_at"),
|
||||
createMandatoryTimestampCriterionOption("updated_at"),
|
||||
CustomFieldsCriterionOption,
|
||||
];
|
||||
|
||||
export const SceneListFilterOptions = new ListFilterOptions(
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { ParentStudiosCriterionOption } from "./criteria/studios";
|
|||
import { TagsCriterionOption } from "./criteria/tags";
|
||||
import { ListFilterOptions } from "./filter-options";
|
||||
import { DisplayMode } from "./types";
|
||||
import { CustomFieldsCriterionOption } from "./criteria/custom-fields";
|
||||
|
||||
const defaultSortBy = "name";
|
||||
const sortByOptions = [
|
||||
|
|
@ -67,6 +68,7 @@ const criterionOptions = [
|
|||
),
|
||||
createMandatoryTimestampCriterionOption("created_at"),
|
||||
createMandatoryTimestampCriterionOption("updated_at"),
|
||||
CustomFieldsCriterionOption,
|
||||
];
|
||||
|
||||
export const StudioListFilterOptions = new ListFilterOptions(
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
} from "./criteria/tags";
|
||||
import { FavoriteTagCriterionOption } from "./criteria/favorite";
|
||||
import { StashIDCriterionOption } from "./criteria/stash-ids";
|
||||
import { CustomFieldsCriterionOption } from "./criteria/custom-fields";
|
||||
|
||||
const defaultSortBy = "name";
|
||||
const sortByOptions = ["name", "random", "scenes_duration"]
|
||||
|
|
@ -77,6 +78,7 @@ const criterionOptions = [
|
|||
new MandatoryNumberCriterionOption("sub_tag_count", "child_count"),
|
||||
createMandatoryTimestampCriterionOption("created_at"),
|
||||
createMandatoryTimestampCriterionOption("updated_at"),
|
||||
CustomFieldsCriterionOption,
|
||||
];
|
||||
|
||||
export const TagListFilterOptions = new ListFilterOptions(
|
||||
|
|
|
|||
Loading…
Reference in a new issue