mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 16:34:02 +01:00
Date picker (#3572)
* Add date picker dependency * Add DateInput component * Add DateInput to edit panels * Add DateInput to DateFilter * Add time to DateInput and add to Timestamp filter * Use calendar icon for button
This commit is contained in:
parent
b602ed2381
commit
b6b275edc8
14 changed files with 458 additions and 73 deletions
|
|
@ -49,6 +49,7 @@
|
|||
"normalize-url": "^4.5.1",
|
||||
"react": "^17.0.2",
|
||||
"react-bootstrap": "^1.6.6",
|
||||
"react-datepicker": "^4.10.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-intl": "^6.2.8",
|
||||
|
|
@ -83,6 +84,7 @@
|
|||
"@types/mousetrap": "^1.6.11",
|
||||
"@types/node": "^18.13.0",
|
||||
"@types/react": "^17.0.53",
|
||||
"@types/react-datepicker": "^4.10.0",
|
||||
"@types/react-dom": "^17.0.19",
|
||||
"@types/react-helmet": "^6.1.6",
|
||||
"@types/react-router-bootstrap": "^0.24.5",
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import { galleryTitle } from "src/core/galleries";
|
|||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
import { DateInput } from "src/components/Shared/DateInput";
|
||||
|
||||
interface IProps {
|
||||
gallery: Partial<GQL.GalleryDataFragment>;
|
||||
|
|
@ -458,11 +459,18 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
{renderTextField(
|
||||
"date",
|
||||
intl.formatMessage({ id: "date" }),
|
||||
"YYYY-MM-DD"
|
||||
)}
|
||||
<Form.Group controlId="date" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "date" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<DateInput
|
||||
value={formik.values.date}
|
||||
onValueChange={(value) => formik.setFieldValue("date", value)}
|
||||
error={formik.errors.date}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="rating" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "rating" }),
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
|||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
import { DateInput } from "src/components/Shared/DateInput";
|
||||
|
||||
interface IProps {
|
||||
image: GQL.ImageDataFragment;
|
||||
|
|
@ -205,11 +206,18 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
{renderTextField(
|
||||
"date",
|
||||
intl.formatMessage({ id: "date" }),
|
||||
"YYYY-MM-DD"
|
||||
)}
|
||||
<Form.Group controlId="date" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "date" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<DateInput
|
||||
value={formik.values.date}
|
||||
onValueChange={(value) => formik.setFieldValue("date", value)}
|
||||
error={formik.errors.date}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="rating" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "rating" }),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useIntl } from "react-intl";
|
|||
import { CriterionModifier } from "../../../core/generated-graphql";
|
||||
import { IDateValue } from "../../../models/list-filter/types";
|
||||
import { Criterion } from "../../../models/list-filter/criteria/criterion";
|
||||
import { DateInput } from "src/components/Shared/DateInput";
|
||||
|
||||
interface IDateFilterProps {
|
||||
criterion: Criterion<IDateValue>;
|
||||
|
|
@ -18,11 +19,7 @@ export const DateFilter: React.FC<IDateFilterProps> = ({
|
|||
|
||||
const { value } = criterion;
|
||||
|
||||
function onChanged(
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
property: "value" | "value2"
|
||||
) {
|
||||
const newValue = event.target.value;
|
||||
function onChanged(newValue: string, property: "value" | "value2") {
|
||||
const valueCopy = { ...value };
|
||||
|
||||
valueCopy[property] = newValue;
|
||||
|
|
@ -36,16 +33,10 @@ export const DateFilter: React.FC<IDateFilterProps> = ({
|
|||
) {
|
||||
equalsControl = (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
className="btn-secondary"
|
||||
type="text"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onChanged(e, "value")
|
||||
}
|
||||
<DateInput
|
||||
value={value?.value ?? ""}
|
||||
placeholder={
|
||||
intl.formatMessage({ id: "criterion.value" }) + " (YYYY-MM-DD)"
|
||||
}
|
||||
onValueChange={(v) => onChanged(v, "value")}
|
||||
placeholder={intl.formatMessage({ id: "criterion.value" })}
|
||||
/>
|
||||
</Form.Group>
|
||||
);
|
||||
|
|
@ -59,17 +50,10 @@ export const DateFilter: React.FC<IDateFilterProps> = ({
|
|||
) {
|
||||
lowerControl = (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
className="btn-secondary"
|
||||
type="text"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onChanged(e, "value")
|
||||
}
|
||||
<DateInput
|
||||
value={value?.value ?? ""}
|
||||
placeholder={
|
||||
intl.formatMessage({ id: "criterion.greater_than" }) +
|
||||
" (YYYY-MM-DD)"
|
||||
}
|
||||
onValueChange={(v) => onChanged(v, "value")}
|
||||
placeholder={intl.formatMessage({ id: "criterion.greater_than" })}
|
||||
/>
|
||||
</Form.Group>
|
||||
);
|
||||
|
|
@ -83,25 +67,21 @@ export const DateFilter: React.FC<IDateFilterProps> = ({
|
|||
) {
|
||||
upperControl = (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
className="btn-secondary"
|
||||
type="text"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onChanged(
|
||||
e,
|
||||
criterion.modifier === CriterionModifier.LessThan
|
||||
? "value"
|
||||
: "value2"
|
||||
)
|
||||
}
|
||||
<DateInput
|
||||
value={
|
||||
(criterion.modifier === CriterionModifier.LessThan
|
||||
? value?.value
|
||||
: value?.value2) ?? ""
|
||||
}
|
||||
placeholder={
|
||||
intl.formatMessage({ id: "criterion.less_than" }) + " (YYYY-MM-DD)"
|
||||
onValueChange={(v) =>
|
||||
onChanged(
|
||||
v,
|
||||
criterion.modifier === CriterionModifier.LessThan
|
||||
? "value"
|
||||
: "value2"
|
||||
)
|
||||
}
|
||||
placeholder={intl.formatMessage({ id: "criterion.less_than" })}
|
||||
/>
|
||||
</Form.Group>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useIntl } from "react-intl";
|
|||
import { CriterionModifier } from "../../../core/generated-graphql";
|
||||
import { ITimestampValue } from "../../../models/list-filter/types";
|
||||
import { Criterion } from "../../../models/list-filter/criteria/criterion";
|
||||
import { DateInput } from "src/components/Shared/DateInput";
|
||||
|
||||
interface ITimestampFilterProps {
|
||||
criterion: Criterion<ITimestampValue>;
|
||||
|
|
@ -18,11 +19,7 @@ export const TimestampFilter: React.FC<ITimestampFilterProps> = ({
|
|||
|
||||
const { value } = criterion;
|
||||
|
||||
function onChanged(
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
property: "value" | "value2"
|
||||
) {
|
||||
const newValue = event.target.value;
|
||||
function onChanged(newValue: string, property: "value" | "value2") {
|
||||
const valueCopy = { ...value };
|
||||
|
||||
valueCopy[property] = newValue;
|
||||
|
|
@ -36,7 +33,13 @@ export const TimestampFilter: React.FC<ITimestampFilterProps> = ({
|
|||
) {
|
||||
equalsControl = (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
<DateInput
|
||||
value={value?.value ?? ""}
|
||||
onValueChange={(v) => onChanged(v, "value")}
|
||||
placeholder={intl.formatMessage({ id: "criterion.value" })}
|
||||
isTime
|
||||
/>
|
||||
{/* <Form.Control
|
||||
className="btn-secondary"
|
||||
type="text"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
|
|
@ -47,7 +50,7 @@ export const TimestampFilter: React.FC<ITimestampFilterProps> = ({
|
|||
intl.formatMessage({ id: "criterion.value" }) +
|
||||
" (YYYY-MM-DD HH:MM)"
|
||||
}
|
||||
/>
|
||||
/> */}
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
|
|
@ -60,7 +63,13 @@ export const TimestampFilter: React.FC<ITimestampFilterProps> = ({
|
|||
) {
|
||||
lowerControl = (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
<DateInput
|
||||
value={value?.value ?? ""}
|
||||
onValueChange={(v) => onChanged(v, "value")}
|
||||
placeholder={intl.formatMessage({ id: "criterion.greater_than" })}
|
||||
isTime
|
||||
/>
|
||||
{/* <Form.Control
|
||||
className="btn-secondary"
|
||||
type="text"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
|
|
@ -71,7 +80,7 @@ export const TimestampFilter: React.FC<ITimestampFilterProps> = ({
|
|||
intl.formatMessage({ id: "criterion.greater_than" }) +
|
||||
" (YYYY-MM-DD HH:MM)"
|
||||
}
|
||||
/>
|
||||
/> */}
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
|
|
@ -84,7 +93,24 @@ export const TimestampFilter: React.FC<ITimestampFilterProps> = ({
|
|||
) {
|
||||
upperControl = (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
<DateInput
|
||||
value={
|
||||
(criterion.modifier === CriterionModifier.LessThan
|
||||
? value?.value
|
||||
: value?.value2) ?? ""
|
||||
}
|
||||
onValueChange={(v) =>
|
||||
onChanged(
|
||||
v,
|
||||
criterion.modifier === CriterionModifier.LessThan
|
||||
? "value"
|
||||
: "value2"
|
||||
)
|
||||
}
|
||||
placeholder={intl.formatMessage({ id: "criterion.less_than" })}
|
||||
isTime
|
||||
/>
|
||||
{/* <Form.Control
|
||||
className="btn-secondary"
|
||||
type="text"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
|
|
@ -104,7 +130,7 @@ export const TimestampFilter: React.FC<ITimestampFilterProps> = ({
|
|||
intl.formatMessage({ id: "criterion.less_than" }) +
|
||||
" (YYYY-MM-DD HH:MM)"
|
||||
}
|
||||
/>
|
||||
/> */}
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { MovieScrapeDialog } from "./MovieScrapeDialog";
|
|||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
import { DateInput } from "src/components/Shared/DateInput";
|
||||
|
||||
interface IMovieEditPanel {
|
||||
movie: Partial<GQL.MovieDataFragment>;
|
||||
|
|
@ -410,11 +411,18 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
{renderTextField(
|
||||
"date",
|
||||
intl.formatMessage({ id: "date" }),
|
||||
"YYYY-MM-DD"
|
||||
)}
|
||||
<Form.Group controlId="date" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "date" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<DateInput
|
||||
value={formik.values.date}
|
||||
onValueChange={(value) => formik.setFieldValue("date", value)}
|
||||
error={formik.errors.date}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="studio" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import {
|
|||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { StringListInput } from "src/components/Shared/StringListInput";
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
import { DateInput } from "src/components/Shared/DateInput";
|
||||
|
||||
const isScraper = (
|
||||
scraper: GQL.Scraper | GQL.StashBox
|
||||
|
|
@ -927,8 +928,35 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
{renderField("birthdate", { placeholder: "YYYY-MM-DD" })}
|
||||
{renderField("death_date", { placeholder: "YYYY-MM-DD" })}
|
||||
<Form.Group controlId="birthdate" as={Row}>
|
||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||
<FormattedMessage id="birthdate" />
|
||||
</Form.Label>
|
||||
<Col xs={fieldXS} xl={fieldXL}>
|
||||
<DateInput
|
||||
value={formik.values.birthdate}
|
||||
onValueChange={(value) =>
|
||||
formik.setFieldValue("birthdate", value)
|
||||
}
|
||||
error={formik.errors.birthdate}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="death_date" as={Row}>
|
||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||
<FormattedMessage id="death_date" />
|
||||
</Form.Label>
|
||||
<Col xs={fieldXS} xl={fieldXL}>
|
||||
<DateInput
|
||||
value={formik.values.death_date}
|
||||
onValueChange={(value) =>
|
||||
formik.setFieldValue("death_date", value)
|
||||
}
|
||||
error={formik.errors.death_date}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group as={Row}>
|
||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ import { galleryTitle } from "src/core/galleries";
|
|||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
import { lazyComponent } from "src/utils/lazyComponent";
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
import { DateInput } from "src/components/Shared/DateInput";
|
||||
|
||||
const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog"));
|
||||
const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal"));
|
||||
|
|
@ -769,11 +770,20 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
{renderTextField(
|
||||
"date",
|
||||
intl.formatMessage({ id: "date" }),
|
||||
"YYYY-MM-DD"
|
||||
)}
|
||||
|
||||
<Form.Group controlId="date" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "date" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<DateInput
|
||||
value={formik.values.date}
|
||||
onValueChange={(value) => formik.setFieldValue("date", value)}
|
||||
error={formik.errors.date}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
{renderTextField(
|
||||
"director",
|
||||
intl.formatMessage({ id: "director" })
|
||||
|
|
|
|||
105
ui/v2.5/src/components/Shared/DateInput.tsx
Normal file
105
ui/v2.5/src/components/Shared/DateInput.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import { faCalendar } from "@fortawesome/free-regular-svg-icons";
|
||||
import React, { useMemo } from "react";
|
||||
import { Button, InputGroup, Form } from "react-bootstrap";
|
||||
import ReactDatePicker from "react-datepicker";
|
||||
import TextUtils from "src/utils/text";
|
||||
import { Icon } from "./Icon";
|
||||
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
interface IProps {
|
||||
disabled?: boolean;
|
||||
value: string | undefined;
|
||||
isTime?: boolean;
|
||||
onValueChange(value: string): void;
|
||||
placeholder?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const DateInput: React.FC<IProps> = (props: IProps) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const date = useMemo(() => {
|
||||
const toDate = props.isTime
|
||||
? TextUtils.stringToFuzzyDateTime
|
||||
: TextUtils.stringToFuzzyDate;
|
||||
if (props.value) {
|
||||
const ret = toDate(props.value);
|
||||
if (!ret || isNaN(ret.getTime())) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
}, [props.value, props.isTime]);
|
||||
|
||||
function maybeRenderButton() {
|
||||
if (!props.disabled) {
|
||||
const ShowPickerButton = ({
|
||||
onClick,
|
||||
}: {
|
||||
onClick: (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => void;
|
||||
}) => (
|
||||
<Button variant="secondary" onClick={onClick}>
|
||||
<Icon icon={faCalendar} />
|
||||
</Button>
|
||||
);
|
||||
|
||||
const dateToString = props.isTime
|
||||
? TextUtils.dateTimeToString
|
||||
: TextUtils.dateToString;
|
||||
|
||||
return (
|
||||
<ReactDatePicker
|
||||
selected={date}
|
||||
onChange={(v) => {
|
||||
props.onValueChange(v ? dateToString(v) : "");
|
||||
}}
|
||||
customInput={React.createElement(ShowPickerButton)}
|
||||
showMonthDropdown
|
||||
showYearDropdown
|
||||
scrollableMonthYearDropdown
|
||||
scrollableYearDropdown
|
||||
maxDate={new Date()}
|
||||
yearDropdownItemNumber={100}
|
||||
portalId="date-picker-portal"
|
||||
showTimeSelect={props.isTime}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const placeholderText = intl.formatMessage({
|
||||
id: props.isTime ? "datetime_format" : "date_format",
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<InputGroup hasValidation>
|
||||
<Form.Control
|
||||
className="date-input text-input"
|
||||
disabled={props.disabled}
|
||||
value={props.value}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
props.onValueChange(e.currentTarget.value)
|
||||
}
|
||||
placeholder={
|
||||
!props.disabled
|
||||
? props.placeholder
|
||||
? `${props.placeholder} (${placeholderText})`
|
||||
: placeholderText
|
||||
: undefined
|
||||
}
|
||||
isInvalid={!!props.error}
|
||||
/>
|
||||
<InputGroup.Append>{maybeRenderButton()}</InputGroup.Append>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{props.error}
|
||||
</Form.Control.Feedback>
|
||||
</InputGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -323,3 +323,97 @@ button.collapse-button.btn-primary:not(:disabled):not(.disabled):active {
|
|||
}
|
||||
/* stylelint-enable */
|
||||
}
|
||||
|
||||
.date-input.form-control:focus {
|
||||
// z-index gets set to 3 in input groups
|
||||
z-index: inherit;
|
||||
}
|
||||
|
||||
/* stylelint-disable */
|
||||
div.react-datepicker {
|
||||
background-color: $body-bg;
|
||||
border-color: $card-bg;
|
||||
color: $text-color;
|
||||
|
||||
.react-datepicker__header,
|
||||
.react-datepicker-time__header {
|
||||
background-color: $secondary;
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
.react-datepicker__day {
|
||||
color: $text-color;
|
||||
&.react-datepicker__day--disabled {
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(138, 155, 168, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
div.react-datepicker__time-container div.react-datepicker__time {
|
||||
background-color: $body-bg;
|
||||
color: $text-color;
|
||||
|
||||
ul.react-datepicker__time-list li.react-datepicker__time-list-item:hover {
|
||||
background-color: rgba(138, 155, 168, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.react-datepicker__day-name {
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
// replace the current month with the dropdowns
|
||||
.react-datepicker__current-month {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.react-datepicker__triangle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.react-datepicker__month-dropdown-container {
|
||||
margin-left: 0;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.react-datepicker__year-dropdown-container {
|
||||
margin-left: 0.25rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.react-datepicker__month-dropdown-container
|
||||
.react-datepicker__month-read-view,
|
||||
.react-datepicker__year-dropdown-container .react-datepicker__year-read-view {
|
||||
font-weight: bold;
|
||||
font-size: 0.944rem;
|
||||
|
||||
// react-datepicker hides these fields when the dropdown is shown
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
// hide the dropdown arrows
|
||||
.react-datepicker__month-dropdown-container
|
||||
.react-datepicker__month-read-view--down-arrow,
|
||||
.react-datepicker__year-dropdown-container
|
||||
.react-datepicker__year-read-view--down-arrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.react-datepicker__year-dropdown,
|
||||
.react-datepicker__month-dropdown {
|
||||
background-color: $body-bg;
|
||||
|
||||
.react-datepicker__year-option:hover,
|
||||
.react-datepicker__month-option:hover {
|
||||
background-color: #8a9ba826;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* stylelint-enable */
|
||||
|
||||
#date-picker-portal .react-datepicker-popper {
|
||||
z-index: 1600;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ Once migrated, these files can be deleted. The files can be optionally deleted d
|
|||
* Added toggleable favorite button to Performer cards. ([#3369](https://github.com/stashapp/stash/pull/3369))
|
||||
|
||||
### 🎨 Improvements
|
||||
* Added date/time pickers for date and timestamp fields. ([#3572](https://github.com/stashapp/stash/pull/3572))
|
||||
* Added folder browser to path filter UI. ([#3570](https://github.com/stashapp/stash/pull/3570))
|
||||
* Include Organized flag in merge dialog. ([#3565](https://github.com/stashapp/stash/pull/3565))
|
||||
* Scene cover generation is now optional during scanning, and can be generated using the Generate task. ([#3187](https://github.com/stashapp/stash/pull/3187))
|
||||
|
|
|
|||
|
|
@ -728,6 +728,8 @@
|
|||
"custom": "Custom",
|
||||
"date": "Date",
|
||||
"death_date": "Death Date",
|
||||
"date_format": "YYYY-MM-DD",
|
||||
"datetime_format": "YYYY-MM-DD HH:MM",
|
||||
"death_year": "Death Year",
|
||||
"descending": "Descending",
|
||||
"description": "Description",
|
||||
|
|
|
|||
|
|
@ -189,6 +189,70 @@ const stringToDate = (dateString: string) => {
|
|||
return new Date(year, monthIndex, day, 0, 0, 0, 0);
|
||||
};
|
||||
|
||||
const stringToFuzzyDate = (dateString: string) => {
|
||||
if (!dateString) return null;
|
||||
|
||||
const parts = dateString.split("-");
|
||||
// Invalid date string
|
||||
let year = Number(parts[0]);
|
||||
if (isNaN(year)) year = new Date().getFullYear();
|
||||
let monthIndex = 0;
|
||||
if (parts.length > 1) {
|
||||
monthIndex = Math.max(0, Number(parts[1]) - 1);
|
||||
if (monthIndex > 11 || isNaN(monthIndex)) monthIndex = 0;
|
||||
}
|
||||
let day = 1;
|
||||
if (parts.length > 2) {
|
||||
day = Number(parts[2]);
|
||||
if (day > 31 || isNaN(day)) day = 1;
|
||||
}
|
||||
|
||||
return new Date(year, monthIndex, day, 0, 0, 0, 0);
|
||||
};
|
||||
|
||||
const stringToFuzzyDateTime = (dateString: string) => {
|
||||
if (!dateString) return null;
|
||||
|
||||
const dateTime = dateString.split(" ");
|
||||
|
||||
let date: Date | null = null;
|
||||
if (dateTime.length > 0) {
|
||||
date = stringToFuzzyDate(dateTime[0]);
|
||||
}
|
||||
|
||||
if (!date) {
|
||||
date = new Date();
|
||||
}
|
||||
|
||||
if (dateTime.length > 1) {
|
||||
const timeParts = dateTime[1].split(":");
|
||||
if (date && timeParts.length > 0) {
|
||||
date.setHours(Number(timeParts[0]));
|
||||
}
|
||||
if (date && timeParts.length > 1) {
|
||||
date.setMinutes(Number(timeParts[1]));
|
||||
}
|
||||
if (date && timeParts.length > 2) {
|
||||
date.setSeconds(Number(timeParts[2]));
|
||||
}
|
||||
}
|
||||
|
||||
return date;
|
||||
};
|
||||
|
||||
function dateToString(date: Date) {
|
||||
return `${date.getFullYear()}-${(date.getMonth() + 1)
|
||||
.toString()
|
||||
.padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function dateTimeToString(date: Date) {
|
||||
return `${dateToString(date)} ${date
|
||||
.getHours()
|
||||
.toString()
|
||||
.padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
const getAge = (dateString?: string | null, fromDateString?: string | null) => {
|
||||
if (!dateString) return 0;
|
||||
|
||||
|
|
@ -355,6 +419,10 @@ const TextUtils = {
|
|||
secondsToTimestamp,
|
||||
fileNameFromPath,
|
||||
stringToDate,
|
||||
stringToFuzzyDate,
|
||||
stringToFuzzyDateTime,
|
||||
dateToString,
|
||||
dateTimeToString,
|
||||
age: getAge,
|
||||
bitRate,
|
||||
resolution,
|
||||
|
|
|
|||
|
|
@ -2139,7 +2139,7 @@
|
|||
tslib "^2.4.1"
|
||||
webcrypto-core "^1.7.4"
|
||||
|
||||
"@popperjs/core@^2.11.6":
|
||||
"@popperjs/core@^2.11.6", "@popperjs/core@^2.9.2":
|
||||
version "2.11.6"
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45"
|
||||
integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==
|
||||
|
|
@ -2339,6 +2339,16 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
|
||||
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
|
||||
|
||||
"@types/react-datepicker@^4.10.0":
|
||||
version "4.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-datepicker/-/react-datepicker-4.10.0.tgz#fcb0e6a7787491bf2f37fbda2b537062608a0056"
|
||||
integrity sha512-Cq+ks20vBIU6XN67TbkCHu8M7V46Y6vJrKE2n+8q/GfueJyWWTIKeC3Z7cz/d+qxGDq/VCrqA929R0U4lNuztg==
|
||||
dependencies:
|
||||
"@popperjs/core" "^2.9.2"
|
||||
"@types/react" "*"
|
||||
date-fns "^2.0.1"
|
||||
react-popper "^2.2.5"
|
||||
|
||||
"@types/react-dom@^17.0.19":
|
||||
version "17.0.19"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.19.tgz#36feef3aa35d045cacd5ed60fe0eef5272f19492"
|
||||
|
|
@ -3303,7 +3313,7 @@ chardet@^0.7.0:
|
|||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
classnames@^2.2.5, classnames@^2.3.1, classnames@^2.3.2:
|
||||
classnames@^2.2.5, classnames@^2.2.6, classnames@^2.3.1, classnames@^2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
|
||||
integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
|
||||
|
|
@ -3559,6 +3569,11 @@ dataloader@2.2.2:
|
|||
resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-2.2.2.tgz#216dc509b5abe39d43a9b9d97e6e5e473dfbe3e0"
|
||||
integrity sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g==
|
||||
|
||||
date-fns@^2.0.1, date-fns@^2.24.0:
|
||||
version "2.29.3"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8"
|
||||
integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==
|
||||
|
||||
debounce@^1.2.0:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5"
|
||||
|
|
@ -6449,6 +6464,18 @@ react-bootstrap@^1.6.6:
|
|||
uncontrollable "^7.2.1"
|
||||
warning "^4.0.3"
|
||||
|
||||
react-datepicker@^4.10.0:
|
||||
version "4.10.0"
|
||||
resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-4.10.0.tgz#3f386ac5873dac5ea56544e51cdc01109938796c"
|
||||
integrity sha512-6IfBCZyWj54ZZGLmEZJ9c4Yph0s9MVfEGDC2evOvf9AmVz+RRcfP2Czqad88Ff9wREbcbqa4dk7IFYeXF1d3Ag==
|
||||
dependencies:
|
||||
"@popperjs/core" "^2.9.2"
|
||||
classnames "^2.2.6"
|
||||
date-fns "^2.24.0"
|
||||
prop-types "^15.7.2"
|
||||
react-onclickoutside "^6.12.2"
|
||||
react-popper "^2.3.0"
|
||||
|
||||
react-dom@^17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
||||
|
|
@ -6463,6 +6490,11 @@ react-fast-compare@^2.0.1:
|
|||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
|
||||
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
|
||||
|
||||
react-fast-compare@^3.0.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.1.tgz#53933d9e14f364281d6cba24bfed7a4afb808b5f"
|
||||
integrity sha512-xTYf9zFim2pEif/Fw16dBiXpe0hoy5PxcD8+OwBnTtNLfIm3g6WxhKNurY+6OmdH1u6Ta/W/Vl6vjbYP1MFnDg==
|
||||
|
||||
react-fast-compare@^3.1.1:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
|
||||
|
|
@ -6504,6 +6536,11 @@ react-lifecycles-compat@^3.0.4:
|
|||
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
||||
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
|
||||
|
||||
react-onclickoutside@^6.12.2:
|
||||
version "6.12.2"
|
||||
resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.12.2.tgz#8e6cf80c7d17a79f2c908399918158a7b02dda01"
|
||||
integrity sha512-NMXGa223OnsrGVp5dJHkuKxQ4czdLmXSp5jSV9OqiCky9LOpPATn3vLldc+q5fK3gKbEHvr7J1u0yhBh/xYkpA==
|
||||
|
||||
react-overlays@^5.1.2:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/react-overlays/-/react-overlays-5.2.1.tgz#49dc007321adb6784e1f212403f0fb37a74ab86b"
|
||||
|
|
@ -6526,6 +6563,14 @@ react-photo-gallery@^8.0.0:
|
|||
prop-types "~15.7.2"
|
||||
resize-observer-polyfill "^1.5.0"
|
||||
|
||||
react-popper@^2.2.5, react-popper@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.3.0.tgz#17891c620e1320dce318bad9fede46a5f71c70ba"
|
||||
integrity sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==
|
||||
dependencies:
|
||||
react-fast-compare "^3.0.1"
|
||||
warning "^4.0.2"
|
||||
|
||||
react-refresh@^0.14.0:
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e"
|
||||
|
|
@ -7957,7 +8002,7 @@ vite@^4.1.1:
|
|||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
warning@^4.0.0, warning@^4.0.3:
|
||||
warning@^4.0.0, warning@^4.0.2, warning@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
|
||||
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
|
||||
|
|
|
|||
Loading…
Reference in a new issue