Merge branch 'stashapp:develop' into feature_selectbycodec

This commit is contained in:
y-e-r 2024-09-22 02:54:34 -04:00 committed by GitHub
commit b8d11b5788
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 119 additions and 92 deletions

View file

@ -74,7 +74,7 @@ func (m *schema32Migrator) migrateFolders(ctx context.Context) error {
query += fmt.Sprintf("ORDER BY `folders`.`id` LIMIT %d", limit) query += fmt.Sprintf("ORDER BY `folders`.`id` LIMIT %d", limit)
rows, err := m.db.Query(query) rows, err := tx.Query(query)
if err != nil { if err != nil {
return err return err
} }
@ -94,12 +94,12 @@ func (m *schema32Migrator) migrateFolders(ctx context.Context) error {
count++ count++
parent := filepath.Dir(p) parent := filepath.Dir(p)
parentID, zipFileID, err := m.createFolderHierarchy(parent) parentID, zipFileID, err := m.createFolderHierarchy(tx, parent)
if err != nil { if err != nil {
return err return err
} }
_, err = m.db.Exec("UPDATE `folders` SET `parent_folder_id` = ?, `zip_file_id` = ? WHERE `id` = ?", parentID, zipFileID, id) _, err = tx.Exec("UPDATE `folders` SET `parent_folder_id` = ?, `zip_file_id` = ? WHERE `id` = ?", parentID, zipFileID, id)
if err != nil { if err != nil {
return err return err
} }
@ -153,7 +153,7 @@ func (m *schema32Migrator) migrateFiles(ctx context.Context) error {
query += fmt.Sprintf("ORDER BY `id` LIMIT %d", limit) query += fmt.Sprintf("ORDER BY `id` LIMIT %d", limit)
if err := m.withTxn(ctx, func(tx *sqlx.Tx) error { if err := m.withTxn(ctx, func(tx *sqlx.Tx) error {
rows, err := m.db.Query(query) rows, err := tx.Query(query)
if err != nil { if err != nil {
return err return err
} }
@ -178,12 +178,12 @@ func (m *schema32Migrator) migrateFiles(ctx context.Context) error {
parent := filepath.Dir(p) parent := filepath.Dir(p)
basename := filepath.Base(p) basename := filepath.Base(p)
if parent != "." { if parent != "." {
parentID, zipFileID, err := m.createFolderHierarchy(parent) parentID, zipFileID, err := m.createFolderHierarchy(tx, parent)
if err != nil { if err != nil {
return err return err
} }
_, err = m.db.Exec("UPDATE `files` SET `parent_folder_id` = ?, `zip_file_id` = ?, `basename` = ? WHERE `id` = ?", parentID, zipFileID, basename, id) _, err = tx.Exec("UPDATE `files` SET `parent_folder_id` = ?, `zip_file_id` = ?, `basename` = ? WHERE `id` = ?", parentID, zipFileID, basename, id)
if err != nil { if err != nil {
return fmt.Errorf("migrating file %s: %w", p, err) return fmt.Errorf("migrating file %s: %w", p, err)
} }
@ -245,16 +245,18 @@ func (m *schema32Migrator) deletePlaceholderFolder(ctx context.Context) error {
return fmt.Errorf("not deleting placeholder folder because it has %d folders", result.Count) return fmt.Errorf("not deleting placeholder folder because it has %d folders", result.Count)
} }
_, err := m.db.Exec("DELETE FROM `folders` WHERE `id` = 1") return m.withTxn(ctx, func(tx *sqlx.Tx) error {
return err _, err := tx.Exec("DELETE FROM `folders` WHERE `id` = 1")
return err
})
} }
func (m *schema32Migrator) createFolderHierarchy(p string) (*int, sql.NullInt64, error) { func (m *schema32Migrator) createFolderHierarchy(tx *sqlx.Tx, p string) (*int, sql.NullInt64, error) {
parent := filepath.Dir(p) parent := filepath.Dir(p)
if parent == p { if parent == p {
// get or create this folder // get or create this folder
return m.getOrCreateFolder(p, nil, sql.NullInt64{}) return m.getOrCreateFolder(tx, p, nil, sql.NullInt64{})
} }
var ( var (
@ -269,23 +271,23 @@ func (m *schema32Migrator) createFolderHierarchy(p string) (*int, sql.NullInt64,
parentID = &foundEntry.id parentID = &foundEntry.id
zipFileID = foundEntry.zipID zipFileID = foundEntry.zipID
} else { } else {
parentID, zipFileID, err = m.createFolderHierarchy(parent) parentID, zipFileID, err = m.createFolderHierarchy(tx, parent)
if err != nil { if err != nil {
return nil, sql.NullInt64{}, err return nil, sql.NullInt64{}, err
} }
} }
return m.getOrCreateFolder(p, parentID, zipFileID) return m.getOrCreateFolder(tx, p, parentID, zipFileID)
} }
func (m *schema32Migrator) getOrCreateFolder(path string, parentID *int, zipFileID sql.NullInt64) (*int, sql.NullInt64, error) { func (m *schema32Migrator) getOrCreateFolder(tx *sqlx.Tx, path string, parentID *int, zipFileID sql.NullInt64) (*int, sql.NullInt64, error) {
foundEntry, ok := m.folderCache[path] foundEntry, ok := m.folderCache[path]
if ok { if ok {
return &foundEntry.id, foundEntry.zipID, nil return &foundEntry.id, foundEntry.zipID, nil
} }
const query = "SELECT `id`, `zip_file_id` FROM `folders` WHERE `path` = ?" const query = "SELECT `id`, `zip_file_id` FROM `folders` WHERE `path` = ?"
rows, err := m.db.Query(query, path) rows, err := tx.Query(query, path)
if err != nil { if err != nil {
return nil, sql.NullInt64{}, err return nil, sql.NullInt64{}, err
} }
@ -314,7 +316,7 @@ func (m *schema32Migrator) getOrCreateFolder(path string, parentID *int, zipFile
} }
now := time.Now() now := time.Now()
result, err := m.db.Exec(insertSQL, path, parentFolderID, zipFileID, time.Time{}, now, now) result, err := tx.Exec(insertSQL, path, parentFolderID, zipFileID, time.Time{}, now, now)
if err != nil { if err != nil {
return nil, sql.NullInt64{}, fmt.Errorf("creating folder %s: %w", path, err) return nil, sql.NullInt64{}, fmt.Errorf("creating folder %s: %w", path, err)
} }

View file

@ -65,7 +65,7 @@ func (m *schema32PreMigrator) migrate(ctx context.Context) error {
query += fmt.Sprintf("ORDER BY `id` LIMIT %d", limit) query += fmt.Sprintf("ORDER BY `id` LIMIT %d", limit)
rows, err := m.db.Query(query) rows, err := tx.Query(query)
if err != nil { if err != nil {
return err return err
} }
@ -100,7 +100,7 @@ func (m *schema32PreMigrator) migrate(ctx context.Context) error {
logger.Infof("Correcting %q gallery to be zip-based.", p) logger.Infof("Correcting %q gallery to be zip-based.", p)
_, err = m.db.Exec("UPDATE `galleries` SET `zip` = '1' WHERE `id` = ?", id) _, err = tx.Exec("UPDATE `galleries` SET `zip` = '1' WHERE `id` = ?", id)
if err != nil { if err != nil {
return err return err
} }

View file

@ -88,7 +88,7 @@ func (m *schema34Migrator) migrateObjects(ctx context.Context, table string, col
query += fmt.Sprintf(" ORDER BY `id` LIMIT %d", limit) query += fmt.Sprintf(" ORDER BY `id` LIMIT %d", limit)
rows, err := m.db.Query(query) rows, err := tx.Query(query)
if err != nil { if err != nil {
return err return err
} }
@ -126,7 +126,7 @@ func (m *schema34Migrator) migrateObjects(ctx context.Context, table string, col
updateSQL := fmt.Sprintf("UPDATE `%s` SET %s WHERE `id` = ?", table, updateList) updateSQL := fmt.Sprintf("UPDATE `%s` SET %s WHERE `id` = ?", table, updateList)
_, err = m.db.Exec(updateSQL, args...) _, err = tx.Exec(updateSQL, args...)
if err != nil { if err != nil {
return err return err
} }

View file

@ -71,7 +71,7 @@ func (m *schema42Migrator) migrate(ctx context.Context) error {
query += fmt.Sprintf(" ORDER BY `performer_id` LIMIT %d", limit) query += fmt.Sprintf(" ORDER BY `performer_id` LIMIT %d", limit)
rows, err := m.db.Query(query) rows, err := tx.Query(query)
if err != nil { if err != nil {
return err return err
} }
@ -92,7 +92,7 @@ func (m *schema42Migrator) migrate(ctx context.Context) error {
gotSome = true gotSome = true
count++ count++
if err := m.migratePerformerAliases(id, aliases); err != nil { if err := m.migratePerformerAliases(tx, id, aliases); err != nil {
return err return err
} }
} }
@ -114,7 +114,7 @@ func (m *schema42Migrator) migrate(ctx context.Context) error {
return nil return nil
} }
func (m *schema42Migrator) migratePerformerAliases(id int, aliases string) error { func (m *schema42Migrator) migratePerformerAliases(tx *sqlx.Tx, id int, aliases string) error {
// split aliases by , or / // split aliases by , or /
aliasList := strings.FieldsFunc(aliases, func(r rune) bool { aliasList := strings.FieldsFunc(aliases, func(r rune) bool {
return strings.ContainsRune(",/", r) return strings.ContainsRune(",/", r)
@ -126,7 +126,7 @@ func (m *schema42Migrator) migratePerformerAliases(id int, aliases string) error
} }
// delete the existing row // delete the existing row
if _, err := m.db.Exec("DELETE FROM `performer_aliases` WHERE `performer_id` = ?", id); err != nil { if _, err := tx.Exec("DELETE FROM `performer_aliases` WHERE `performer_id` = ?", id); err != nil {
return err return err
} }
@ -140,7 +140,7 @@ func (m *schema42Migrator) migratePerformerAliases(id int, aliases string) error
// insert aliases into table // insert aliases into table
for _, alias := range aliasList { for _, alias := range aliasList {
_, err := m.db.Exec("INSERT INTO `performer_aliases` (`performer_id`, `alias`) VALUES (?, ?)", id, alias) _, err := tx.Exec("INSERT INTO `performer_aliases` (`performer_id`, `alias`) VALUES (?, ?)", id, alias)
if err != nil { if err != nil {
return err return err
} }
@ -173,7 +173,7 @@ SELECT id, name FROM performers WHERE performers.name like '% (%)'`
query += fmt.Sprintf(" ORDER BY `id` LIMIT %d", limit) query += fmt.Sprintf(" ORDER BY `id` LIMIT %d", limit)
rows, err := m.db.Query(query) rows, err := tx.Query(query)
if err != nil { if err != nil {
return err return err
} }
@ -194,7 +194,7 @@ SELECT id, name FROM performers WHERE performers.name like '% (%)'`
lastID = id lastID = id
count++ count++
if err := m.massagePerformerName(id, name); err != nil { if err := m.massagePerformerName(tx, id, name); err != nil {
return err return err
} }
} }
@ -220,7 +220,7 @@ SELECT id, name FROM performers WHERE performers.name like '% (%)'`
// the format "name (disambiguation)". // the format "name (disambiguation)".
var performerDisRE = regexp.MustCompile(`^((?:[^(\s]+\s)+)\(([^)]+)\)$`) var performerDisRE = regexp.MustCompile(`^((?:[^(\s]+\s)+)\(([^)]+)\)$`)
func (m *schema42Migrator) massagePerformerName(performerID int, name string) error { func (m *schema42Migrator) massagePerformerName(tx *sqlx.Tx, performerID int, name string) error {
r := performerDisRE.FindStringSubmatch(name) r := performerDisRE.FindStringSubmatch(name)
if len(r) != 3 { if len(r) != 3 {
@ -235,7 +235,7 @@ func (m *schema42Migrator) massagePerformerName(performerID int, name string) er
logger.Infof("Separating %q into %q and disambiguation %q", name, newName, newDis) logger.Infof("Separating %q into %q and disambiguation %q", name, newName, newDis)
_, err := m.db.Exec("UPDATE performers SET name = ?, disambiguation = ? WHERE id = ?", newName, newDis, performerID) _, err := tx.Exec("UPDATE performers SET name = ?, disambiguation = ? WHERE id = ?", newName, newDis, performerID)
if err != nil { if err != nil {
return err return err
} }
@ -266,7 +266,7 @@ SELECT id, name FROM performers WHERE performers.disambiguation IS NULL AND EXIS
query += fmt.Sprintf(" ORDER BY `id` LIMIT %d", limit) query += fmt.Sprintf(" ORDER BY `id` LIMIT %d", limit)
rows, err := m.db.Query(query) rows, err := tx.Query(query)
if err != nil { if err != nil {
return err return err
} }
@ -286,7 +286,7 @@ SELECT id, name FROM performers WHERE performers.disambiguation IS NULL AND EXIS
gotSome = true gotSome = true
count++ count++
if err := m.migrateDuplicatePerformer(id, name); err != nil { if err := m.migrateDuplicatePerformer(tx, id, name); err != nil {
return err return err
} }
} }
@ -308,13 +308,13 @@ SELECT id, name FROM performers WHERE performers.disambiguation IS NULL AND EXIS
return nil return nil
} }
func (m *schema42Migrator) migrateDuplicatePerformer(performerID int, name string) error { func (m *schema42Migrator) migrateDuplicatePerformer(tx *sqlx.Tx, performerID int, name string) error {
// get the highest value of disambiguation for this performer name // get the highest value of disambiguation for this performer name
query := ` query := `
SELECT disambiguation FROM performers WHERE name = ? ORDER BY disambiguation DESC LIMIT 1` SELECT disambiguation FROM performers WHERE name = ? ORDER BY disambiguation DESC LIMIT 1`
var disambiguation sql.NullString var disambiguation sql.NullString
if err := m.db.Get(&disambiguation, query, name); err != nil { if err := tx.Get(&disambiguation, query, name); err != nil {
return err return err
} }
@ -333,7 +333,7 @@ SELECT disambiguation FROM performers WHERE name = ? ORDER BY disambiguation DES
logger.Infof("Adding disambiguation '%d' for performer %q", newDisambiguation, name) logger.Infof("Adding disambiguation '%d' for performer %q", newDisambiguation, name)
_, err := m.db.Exec("UPDATE performers SET disambiguation = ? WHERE id = ?", strconv.Itoa(newDisambiguation), performerID) _, err := tx.Exec("UPDATE performers SET disambiguation = ? WHERE id = ?", strconv.Itoa(newDisambiguation), performerID)
if err != nil { if err != nil {
return err return err
} }

View file

@ -161,7 +161,7 @@ func (m *schema45Migrator) migrateImagesTable(ctx context.Context, options migra
query += fmt.Sprintf(" LIMIT %d", limit) query += fmt.Sprintf(" LIMIT %d", limit)
rows, err := m.db.Query(query) rows, err := tx.Query(query)
if err != nil { if err != nil {
return err return err
} }
@ -191,7 +191,7 @@ func (m *schema45Migrator) migrateImagesTable(ctx context.Context, options migra
image := result[i+1].(*[]byte) image := result[i+1].(*[]byte)
if len(*image) > 0 { if len(*image) > 0 {
if err := m.insertImage(*image, id, options.destTable, col.destCol); err != nil { if err := m.insertImage(tx, *image, id, options.destTable, col.destCol); err != nil {
return err return err
} }
} }
@ -202,7 +202,7 @@ func (m *schema45Migrator) migrateImagesTable(ctx context.Context, options migra
"joinTable": options.joinTable, "joinTable": options.joinTable,
"joinIDCol": options.joinIDCol, "joinIDCol": options.joinIDCol,
}) })
if _, err := m.db.Exec(deleteSQL, id); err != nil { if _, err := tx.Exec(deleteSQL, id); err != nil {
return err return err
} }
} }
@ -224,11 +224,11 @@ func (m *schema45Migrator) migrateImagesTable(ctx context.Context, options migra
return nil return nil
} }
func (m *schema45Migrator) insertImage(data []byte, id int, destTable string, destCol string) error { func (m *schema45Migrator) insertImage(tx *sqlx.Tx, data []byte, id int, destTable string, destCol string) error {
// calculate checksum and insert into blobs table // calculate checksum and insert into blobs table
checksum := md5.FromBytes(data) checksum := md5.FromBytes(data)
if _, err := m.db.Exec("INSERT INTO `blobs` (`checksum`, `blob`) VALUES (?, ?) ON CONFLICT DO NOTHING", checksum, data); err != nil { if _, err := tx.Exec("INSERT INTO `blobs` (`checksum`, `blob`) VALUES (?, ?) ON CONFLICT DO NOTHING", checksum, data); err != nil {
return err return err
} }
@ -237,7 +237,7 @@ func (m *schema45Migrator) insertImage(data []byte, id int, destTable string, de
"destTable": destTable, "destTable": destTable,
"destCol": destCol, "destCol": destCol,
}) })
if _, err := m.db.Exec(updateSQL, checksum, id); err != nil { if _, err := tx.Exec(updateSQL, checksum, id); err != nil {
return err return err
} }

View file

@ -112,7 +112,7 @@ type schema49Migrator struct {
func (m *schema49Migrator) migrateSavedFilters(ctx context.Context) error { func (m *schema49Migrator) migrateSavedFilters(ctx context.Context) error {
if err := m.withTxn(ctx, func(tx *sqlx.Tx) error { if err := m.withTxn(ctx, func(tx *sqlx.Tx) error {
rows, err := m.db.Query("SELECT id, mode, find_filter FROM saved_filters ORDER BY id") rows, err := tx.Query("SELECT id, mode, find_filter FROM saved_filters ORDER BY id")
if err != nil { if err != nil {
return err return err
} }
@ -147,7 +147,7 @@ func (m *schema49Migrator) migrateSavedFilters(ctx context.Context) error {
return fmt.Errorf("failed to get display options for saved filter %s : %w", findFilter, err) return fmt.Errorf("failed to get display options for saved filter %s : %w", findFilter, err)
} }
_, err = m.db.Exec("UPDATE saved_filters SET find_filter = ?, object_filter = ?, ui_options = ? WHERE id = ?", newFindFilter, objectFilter, uiOptions, id) _, err = tx.Exec("UPDATE saved_filters SET find_filter = ?, object_filter = ?, ui_options = ? WHERE id = ?", newFindFilter, objectFilter, uiOptions, id)
if err != nil { if err != nil {
return fmt.Errorf("failed to update saved filter %d: %w", id, err) return fmt.Errorf("failed to update saved filter %d: %w", id, err)
} }

View file

@ -34,7 +34,7 @@ func (m *schema52Migrator) migrate(ctx context.Context) error {
query := "SELECT `folders`.`id`, `folders`.`path`, `parent_folder`.`path` FROM `folders` " + query := "SELECT `folders`.`id`, `folders`.`path`, `parent_folder`.`path` FROM `folders` " +
"INNER JOIN `folders` AS `parent_folder` ON `parent_folder`.`id` = `folders`.`parent_folder_id`" "INNER JOIN `folders` AS `parent_folder` ON `parent_folder`.`id` = `folders`.`parent_folder_id`"
rows, err := m.db.Query(query) rows, err := tx.Query(query)
if err != nil { if err != nil {
return err return err
} }
@ -64,7 +64,7 @@ func (m *schema52Migrator) migrate(ctx context.Context) error {
// ensure the correct path is unique // ensure the correct path is unique
var v int var v int
isEmptyErr := m.db.Get(&v, "SELECT 1 FROM folders WHERE path = ?", correctPath) isEmptyErr := tx.Get(&v, "SELECT 1 FROM folders WHERE path = ?", correctPath)
if isEmptyErr != nil && !errors.Is(isEmptyErr, sql.ErrNoRows) { if isEmptyErr != nil && !errors.Is(isEmptyErr, sql.ErrNoRows) {
return fmt.Errorf("error checking if correct path %s is unique: %w", correctPath, isEmptyErr) return fmt.Errorf("error checking if correct path %s is unique: %w", correctPath, isEmptyErr)
} }
@ -75,7 +75,7 @@ func (m *schema52Migrator) migrate(ctx context.Context) error {
continue continue
} }
if _, err := m.db.Exec("UPDATE folders SET path = ? WHERE id = ?", correctPath, id); err != nil { if _, err := tx.Exec("UPDATE folders SET path = ? WHERE id = ?", correctPath, id); err != nil {
return fmt.Errorf("error updating folder path %s to %s: %w", folderPath, correctPath, err) return fmt.Errorf("error updating folder path %s to %s: %w", folderPath, correctPath, err)
} }
} }

View file

@ -31,7 +31,7 @@ func (m *schema55Migrator) migrate(ctx context.Context) error {
if err := m.withTxn(ctx, func(tx *sqlx.Tx) error { if err := m.withTxn(ctx, func(tx *sqlx.Tx) error {
query := "SELECT DISTINCT `scene_id`, `view_date` FROM `scenes_view_dates`" query := "SELECT DISTINCT `scene_id`, `view_date` FROM `scenes_view_dates`"
rows, err := m.db.Query(query) rows, err := tx.Query(query)
if err != nil { if err != nil {
return err return err
} }
@ -53,7 +53,7 @@ func (m *schema55Migrator) migrate(ctx context.Context) error {
} }
// convert the timestamp to the correct format // convert the timestamp to the correct format
if _, err := m.db.Exec("UPDATE scenes_view_dates SET view_date = ? WHERE view_date = ?", utcTimestamp, viewDate.Timestamp); err != nil { if _, err := tx.Exec("UPDATE scenes_view_dates SET view_date = ? WHERE view_date = ?", utcTimestamp, viewDate.Timestamp); err != nil {
return fmt.Errorf("error correcting view date %s to %s: %w", viewDate.Timestamp, viewDate, err) return fmt.Errorf("error correcting view date %s to %s: %w", viewDate.Timestamp, viewDate, err)
} }
} }

View file

@ -35,7 +35,7 @@ func (m *schema64Migrator) migrate(ctx context.Context) error {
if err := m.withTxn(ctx, func(tx *sqlx.Tx) error { if err := m.withTxn(ctx, func(tx *sqlx.Tx) error {
query := "SELECT DISTINCT `scene_id`, `view_date` FROM `scenes_view_dates`" query := "SELECT DISTINCT `scene_id`, `view_date` FROM `scenes_view_dates`"
rows, err := m.db.Query(query) rows, err := tx.Query(query)
if err != nil { if err != nil {
return err return err
} }
@ -64,7 +64,7 @@ func (m *schema64Migrator) migrate(ctx context.Context) error {
// convert the timestamp to the correct format // convert the timestamp to the correct format
logger.Debugf("correcting view date %q to UTC date %q for scene %d", viewDate.Timestamp, viewDate.Timestamp.UTC(), id) logger.Debugf("correcting view date %q to UTC date %q for scene %d", viewDate.Timestamp, viewDate.Timestamp.UTC(), id)
r, err := m.db.Exec("UPDATE scenes_view_dates SET view_date = ? WHERE scene_id = ? AND (view_date = ? OR view_date = ?)", utcTimestamp, id, viewDate.Timestamp, viewDate) r, err := tx.Exec("UPDATE scenes_view_dates SET view_date = ? WHERE scene_id = ? AND (view_date = ? OR view_date = ?)", utcTimestamp, id, viewDate.Timestamp, viewDate)
if err != nil { if err != nil {
return fmt.Errorf("error correcting view date %s to %s: %w", viewDate.Timestamp, viewDate, err) return fmt.Errorf("error correcting view date %s to %s: %w", viewDate.Timestamp, viewDate, err)
} }

View file

@ -1128,9 +1128,12 @@ func (qb *SceneStore) setSceneSort(query *queryBuilder, findFilter *models.FindF
direction := findFilter.GetDirection() direction := findFilter.GetDirection()
switch sort { switch sort {
case "movie_scene_number", "group_scene_number": case "movie_scene_number":
query.join(groupsScenesTable, "", "scenes.id = groups_scenes.scene_id") query.join(groupsScenesTable, "", "scenes.id = groups_scenes.scene_id")
query.sortAndPagination += getSort("scene_index", direction, groupsScenesTable) query.sortAndPagination += getSort("scene_index", direction, groupsScenesTable)
case "group_scene_number":
query.join(groupsScenesTable, "scene_group", "scenes.id = scene_group.scene_id")
query.sortAndPagination += getSort("scene_index", direction, "scene_group")
case "tag_count": case "tag_count":
query.sortAndPagination += getCountSort(sceneTable, scenesTagsTable, sceneIDColumn, direction) query.sortAndPagination += getCountSort(sceneTable, scenesTagsTable, sceneIDColumn, direction)
case "performer_count": case "performer_count":

View file

@ -4,6 +4,7 @@ import { defineMessages, MessageDescriptor, useIntl } from "react-intl";
import { FilterSelect, SelectObject } from "src/components/Shared/Select"; import { FilterSelect, SelectObject } from "src/components/Shared/Select";
import { Criterion } from "src/models/list-filter/criteria/criterion"; import { Criterion } from "src/models/list-filter/criteria/criterion";
import { IHierarchicalLabelValue } from "src/models/list-filter/types"; import { IHierarchicalLabelValue } from "src/models/list-filter/types";
import { NumberField } from "src/utils/form";
interface IHierarchicalLabelValueFilterProps { interface IHierarchicalLabelValueFilterProps {
criterion: Criterion<IHierarchicalLabelValue>; criterion: Criterion<IHierarchicalLabelValue>;
@ -104,9 +105,8 @@ export const HierarchicalLabelValueFilter: React.FC<
{criterion.value.depth !== 0 && ( {criterion.value.depth !== 0 && (
<Form.Group> <Form.Group>
<Form.Control <NumberField
className="btn-secondary" className="btn-secondary"
type="number"
placeholder={intl.formatMessage(messages.studio_depth)} placeholder={intl.formatMessage(messages.studio_depth)}
onChange={(e) => onChange={(e) =>
onDepthChanged(e.target.value ? parseInt(e.target.value, 10) : -1) onDepthChanged(e.target.value ? parseInt(e.target.value, 10) : -1)

View file

@ -4,6 +4,7 @@ import { useIntl } from "react-intl";
import { CriterionModifier } from "../../../core/generated-graphql"; import { CriterionModifier } from "../../../core/generated-graphql";
import { INumberValue } from "../../../models/list-filter/types"; import { INumberValue } from "../../../models/list-filter/types";
import { NumberCriterion } from "../../../models/list-filter/criteria/criterion"; import { NumberCriterion } from "../../../models/list-filter/criteria/criterion";
import { NumberField } from "src/utils/form";
interface IDurationFilterProps { interface IDurationFilterProps {
criterion: NumberCriterion; criterion: NumberCriterion;
@ -36,9 +37,8 @@ export const NumberFilter: React.FC<IDurationFilterProps> = ({
) { ) {
equalsControl = ( equalsControl = (
<Form.Group> <Form.Group>
<Form.Control <NumberField
className="btn-secondary" className="btn-secondary"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChanged(e, "value") onChanged(e, "value")
} }
@ -57,9 +57,8 @@ export const NumberFilter: React.FC<IDurationFilterProps> = ({
) { ) {
lowerControl = ( lowerControl = (
<Form.Group> <Form.Group>
<Form.Control <NumberField
className="btn-secondary" className="btn-secondary"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChanged(e, "value") onChanged(e, "value")
} }
@ -78,9 +77,8 @@ export const NumberFilter: React.FC<IDurationFilterProps> = ({
) { ) {
upperControl = ( upperControl = (
<Form.Group> <Form.Group>
<Form.Control <NumberField
className="btn-secondary" className="btn-secondary"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChanged( onChanged(
e, e,

View file

@ -4,6 +4,7 @@ import { useIntl } from "react-intl";
import { IPhashDistanceValue } from "../../../models/list-filter/types"; import { IPhashDistanceValue } from "../../../models/list-filter/types";
import { Criterion } from "../../../models/list-filter/criteria/criterion"; import { Criterion } from "../../../models/list-filter/criteria/criterion";
import { CriterionModifier } from "src/core/generated-graphql"; import { CriterionModifier } from "src/core/generated-graphql";
import { NumberField } from "src/utils/form";
interface IPhashFilterProps { interface IPhashFilterProps {
criterion: Criterion<IPhashDistanceValue>; criterion: Criterion<IPhashDistanceValue>;
@ -49,10 +50,9 @@ export const PhashFilter: React.FC<IPhashFilterProps> = ({
{criterion.modifier !== CriterionModifier.IsNull && {criterion.modifier !== CriterionModifier.IsNull &&
criterion.modifier !== CriterionModifier.NotNull && ( criterion.modifier !== CriterionModifier.NotNull && (
<Form.Group> <Form.Group>
<Form.Control <NumberField
className="btn-secondary" className="btn-secondary"
onChange={distanceChanged} onChange={distanceChanged}
type="number"
value={value ? value.distance : ""} value={value ? value.distance : ""}
placeholder={intl.formatMessage({ id: "distance" })} placeholder={intl.formatMessage({ id: "distance" })}
/> />

View file

@ -25,6 +25,7 @@ import { keyboardClickHandler } from "src/utils/keyboard";
import { useDebounce } from "src/hooks/debounce"; import { useDebounce } from "src/hooks/debounce";
import useFocus from "src/utils/focus"; import useFocus from "src/utils/focus";
import ScreenUtils from "src/utils/screen"; import ScreenUtils from "src/utils/screen";
import { NumberField } from "src/utils/form";
interface ISelectedItem { interface ISelectedItem {
item: ILabeledId; item: ILabeledId;
@ -361,9 +362,8 @@ export const HierarchicalObjectsFilter = <
{criterion.value.depth !== 0 && ( {criterion.value.depth !== 0 && (
<Form.Group> <Form.Group>
<Form.Control <NumberField
className="btn-secondary" className="btn-secondary"
type="number"
placeholder={intl.formatMessage(messages.studio_depth)} placeholder={intl.formatMessage(messages.studio_depth)}
onChange={(e) => onChange={(e) =>
onDepthChanged(e.target.value ? parseInt(e.target.value, 10) : -1) onDepthChanged(e.target.value ? parseInt(e.target.value, 10) : -1)

View file

@ -35,6 +35,7 @@ import { FilterButton } from "./Filters/FilterButton";
import { useDebounce } from "src/hooks/debounce"; import { useDebounce } from "src/hooks/debounce";
import { View } from "./views"; import { View } from "./views";
import { ClearableInput } from "../Shared/ClearableInput"; import { ClearableInput } from "../Shared/ClearableInput";
import { useStopWheelScroll } from "src/utils/form";
export function useDebouncedSearchInput( export function useDebouncedSearchInput(
filter: ListFilterModel, filter: ListFilterModel,
@ -126,6 +127,8 @@ export const PageSizeSelector: React.FC<{
} }
}, [customPageSizeShowing, perPageFocus]); }, [customPageSizeShowing, perPageFocus]);
useStopWheelScroll(perPageInput);
const pageSizeOptions = useMemo(() => { const pageSizeOptions = useMemo(() => {
const ret = PAGE_SIZE_OPTIONS.map((o) => { const ret = PAGE_SIZE_OPTIONS.map((o) => {
return { return {
@ -190,6 +193,7 @@ export const PageSizeSelector: React.FC<{
<Popover id="custom_pagesize_popover"> <Popover id="custom_pagesize_popover">
<Form inline> <Form inline>
<InputGroup> <InputGroup>
{/* can't use NumberField because of the ref */}
<Form.Control <Form.Control
type="number" type="number"
min={1} min={1}

View file

@ -12,6 +12,7 @@ import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
import useFocus from "src/utils/focus"; import useFocus from "src/utils/focus";
import { Icon } from "../Shared/Icon"; import { Icon } from "../Shared/Icon";
import { faCheck, faChevronDown } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faChevronDown } from "@fortawesome/free-solid-svg-icons";
import { useStopWheelScroll } from "src/utils/form";
const PageCount: React.FC<{ const PageCount: React.FC<{
totalPages: number; totalPages: number;
@ -32,6 +33,8 @@ const PageCount: React.FC<{
} }
}, [showSelectPage, pageFocus]); }, [showSelectPage, pageFocus]);
useStopWheelScroll(pageInput);
const pageOptions = useMemo(() => { const pageOptions = useMemo(() => {
const maxPagesToShow = 10; const maxPagesToShow = 10;
const min = Math.max(1, currentPage - maxPagesToShow / 2); const min = Math.max(1, currentPage - maxPagesToShow / 2);
@ -98,6 +101,7 @@ const PageCount: React.FC<{
<Popover id="select_page_popover"> <Popover id="select_page_popover">
<Form inline> <Form inline>
<InputGroup> <InputGroup>
{/* can't use NumberField because of the ref */}
<Form.Control <Form.Control
type="number" type="number"
min={1} min={1}

View file

@ -4,6 +4,7 @@ import * as GQL from "src/core/generated-graphql";
import { Form, Row, Col } from "react-bootstrap"; import { Form, Row, Col } from "react-bootstrap";
import { Group, GroupSelect } from "src/components/Groups/GroupSelect"; import { Group, GroupSelect } from "src/components/Groups/GroupSelect";
import cx from "classnames"; import cx from "classnames";
import { NumberField } from "src/utils/form";
export type GroupSceneIndexMap = Map<string, number | undefined>; export type GroupSceneIndexMap = Map<string, number | undefined>;
@ -92,9 +93,8 @@ export const SceneGroupTable: React.FC<IProps> = (props) => {
/> />
</Col> </Col>
<Col xs={3}> <Col xs={3}>
<Form.Control <NumberField
className="text-input" className="text-input"
type="number"
value={m.scene_index ?? ""} value={m.scene_index ?? ""}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateFieldChanged( updateFieldChanged(

View file

@ -2,6 +2,7 @@ import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { Form } from "react-bootstrap"; import { Form } from "react-bootstrap";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { NumberField } from "src/utils/form";
export type VideoPreviewSettingsInput = Pick< export type VideoPreviewSettingsInput = Pick<
GQL.ConfigGeneralInput, GQL.ConfigGeneralInput,
@ -44,9 +45,8 @@ export const VideoPreviewInput: React.FC<IVideoPreviewInput> = ({
id: "dialogs.scene_gen.preview_seg_count_head", id: "dialogs.scene_gen.preview_seg_count_head",
})} })}
</h6> </h6>
<Form.Control <NumberField
className="text-input" className="text-input"
type="number"
value={previewSegments?.toString() ?? 1} value={previewSegments?.toString() ?? 1}
min={1} min={1}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
@ -71,9 +71,8 @@ export const VideoPreviewInput: React.FC<IVideoPreviewInput> = ({
id: "dialogs.scene_gen.preview_seg_duration_head", id: "dialogs.scene_gen.preview_seg_duration_head",
})} })}
</h6> </h6>
<Form.Control <NumberField
className="text-input" className="text-input"
type="number"
value={previewSegmentDuration?.toString() ?? 0} value={previewSegmentDuration?.toString() ?? 0}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
set({ set({

View file

@ -6,6 +6,7 @@ import { Icon } from "../Shared/Icon";
import { StringListInput } from "../Shared/StringListInput"; import { StringListInput } from "../Shared/StringListInput";
import { PatchComponent } from "src/patch"; import { PatchComponent } from "src/patch";
import { useSettings, useSettingsOptional } from "./context"; import { useSettings, useSettingsOptional } from "./context";
import { NumberField } from "src/utils/form";
interface ISetting { interface ISetting {
id?: string; id?: string;
@ -484,9 +485,8 @@ export const NumberSetting: React.FC<INumberSetting> = PatchComponent(
<ModalSetting<number> <ModalSetting<number>
{...props} {...props}
renderField={(value, setValue) => ( renderField={(value, setValue) => (
<Form.Control <NumberField
className="text-input" className="text-input"
type="number"
value={value ?? 0} value={value ?? 0}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setValue(Number.parseInt(e.currentTarget.value || "0", 10)) setValue(Number.parseInt(e.currentTarget.value || "0", 10))

View file

@ -3,6 +3,7 @@ import { Button } from "react-bootstrap";
import { Icon } from "../Icon"; import { Icon } from "../Icon";
import { faPencil, faStar } from "@fortawesome/free-solid-svg-icons"; import { faPencil, faStar } from "@fortawesome/free-solid-svg-icons";
import { useFocusOnce } from "src/utils/focus"; import { useFocusOnce } from "src/utils/focus";
import { useStopWheelScroll } from "src/utils/form";
export interface IRatingNumberProps { export interface IRatingNumberProps {
value: number | null; value: number | null;
@ -26,6 +27,7 @@ export const RatingNumber: React.FC<IRatingNumberProps> = (
const showTextField = !props.disabled && (editing || !props.clickToRate); const showTextField = !props.disabled && (editing || !props.clickToRate);
const [ratingRef] = useFocusOnce(editing, true); const [ratingRef] = useFocusOnce(editing, true);
useStopWheelScroll(ratingRef);
const effectiveValue = editing ? valueStage : props.value; const effectiveValue = editing ? valueStage : props.value;

View file

@ -13,6 +13,7 @@
* Added support for bulk-editing Tags. ([#4925](https://github.com/stashapp/stash/pull/4925)) * Added support for bulk-editing Tags. ([#4925](https://github.com/stashapp/stash/pull/4925))
* Added filter to Scrapers menu. ([#5041](https://github.com/stashapp/stash/pull/5041)) * Added filter to Scrapers menu. ([#5041](https://github.com/stashapp/stash/pull/5041))
* Added ability to set the location of ssl certificate files. ([#4910](https://github.com/stashapp/stash/pull/4910)) * Added ability to set the location of ssl certificate files. ([#4910](https://github.com/stashapp/stash/pull/4910))
* Added option to rescan all files in the Scan task. ([#5254](https://github.com/stashapp/stash/pull/5254))
### 🎨 Improvements ### 🎨 Improvements
* Added button to view sub-studio/sub-tag content on Studio/Tag details pages. ([#5080](https://github.com/stashapp/stash/pull/5080)) * Added button to view sub-studio/sub-tag content on Studio/Tag details pages. ([#5080](https://github.com/stashapp/stash/pull/5080))
@ -38,10 +39,13 @@
* Anonymise now truncates o- and view history data. ([#5166](https://github.com/stashapp/stash/pull/5166)) * Anonymise now truncates o- and view history data. ([#5166](https://github.com/stashapp/stash/pull/5166))
* Fixed issue where using mouse wheel on numeric input fields would scroll the window in addition to changing the value. ([#5199](https://github.com/stashapp/stash/pull/5199)) * Fixed issue where using mouse wheel on numeric input fields would scroll the window in addition to changing the value. ([#5199](https://github.com/stashapp/stash/pull/5199))
* Fixed issue where some o-dates could not be deleted. ([#4971](https://github.com/stashapp/stash/pull/4971)) * Fixed issue where some o-dates could not be deleted. ([#4971](https://github.com/stashapp/stash/pull/4971))
* Fixed handling of symlink zip files. ([#5249](https://github.com/stashapp/stash/pull/5249))
* Fixed default database backup directory being set to the config file directory instead of the database directory. ([#5250](https://github.com/stashapp/stash/pull/5250))
* Added API key to DASH and HLS manifests. ([#5061](https://github.com/stashapp/stash/pull/5061)) * Added API key to DASH and HLS manifests. ([#5061](https://github.com/stashapp/stash/pull/5061))
* Query field no longer focused when selecting items in the filter list on touch devices. ([#5204](https://github.com/stashapp/stash/pull/5204)) * Query field no longer focused when selecting items in the filter list on touch devices. ([#5204](https://github.com/stashapp/stash/pull/5204))
* Fixed weird scrolling behaviour on Gallery detail page on smaller viewports ([#5205](https://github.com/stashapp/stash/pull/5205)) * Fixed weird scrolling behaviour on Gallery detail page on smaller viewports ([#5205](https://github.com/stashapp/stash/pull/5205))
* Performer popover links now correctly link to the applicable scenes/image/gallery query page instead of always going to scenes. ([#5195](https://github.com/stashapp/stash/pull/5195)) * Performer popover links now correctly link to the applicable scenes/image/gallery query page instead of always going to scenes. ([#5195](https://github.com/stashapp/stash/pull/5195))
* Fixed scene player source selector appearing behind the player controls. ([#5229](https://github.com/stashapp/stash/pull/5229))
* Fixed red/green/blue slider values in the Scene Filter panel. ([#5221](https://github.com/stashapp/stash/pull/5221)) * Fixed red/green/blue slider values in the Scene Filter panel. ([#5221](https://github.com/stashapp/stash/pull/5221))
* Play button no longer appears on file-less Scenes. ([#5141](https://github.com/stashapp/stash/pull/5141)) * Play button no longer appears on file-less Scenes. ([#5141](https://github.com/stashapp/stash/pull/5141))
* Fixed transgender icon colouring. ([#5090](https://github.com/stashapp/stash/pull/5090)) * Fixed transgender icon colouring. ([#5090](https://github.com/stashapp/stash/pull/5090))

View file

@ -10,7 +10,7 @@ The text field allows you to search using keywords. Keyword searching matches on
|------|-----------------| |------|-----------------|
| Scene | Title, Details, Path, OSHash, Checksum, Marker titles | | Scene | Title, Details, Path, OSHash, Checksum, Marker titles |
| Image | Title, Path, Checksum | | Image | Title, Path, Checksum |
| Movie | Title | | Group | Title |
| Marker | Title, Scene title | | Marker | Title, Scene title |
| Gallery | Title, Path, Checksum | | Gallery | Title, Path, Checksum |
| Performer | Name, Aliases | | Performer | Name, Aliases |

View file

@ -8,7 +8,7 @@ The metadata given to Stash can be exported into the JSON format. This structure
* `performers` * `performers`
* `scenes` * `scenes`
* `studios` * `studios`
* `movies` * `groups`
## File naming ## File naming
@ -22,7 +22,7 @@ When exported, files are named with different formats depending on the object ty
| Performers | `<name>.json` | | Performers | `<name>.json` |
| Scenes | `<title or first file basename>.<hash>.json` | | Scenes | `<title or first file basename>.<hash>.json` |
| Studios | `<name>.json` | | Studios | `<name>.json` |
| Movies | `<name>.json` | | Groups | `<name>.json` |
Note that the file naming is not significant when importing. All json files will be read from the subdirectories. Note that the file naming is not significant when importing. All json files will be read from the subdirectories.

View file

@ -12,7 +12,7 @@
|-------------------|--------| |-------------------|--------|
| `g s` | Scenes | | `g s` | Scenes |
| `g i` | Images | | `g i` | Images |
| `g v` | Movies | | `g v` | Groups |
| `g k` | Markers | | `g k` | Markers |
| `g l` | Galleries | | `g l` | Galleries |
| `g p` | Performers | | `g p` | Performers |
@ -104,28 +104,28 @@
[//]: # "(| `l` | Focus Gallery selector |)" [//]: # "(| `l` | Focus Gallery selector |)"
[//]: # "(| `u` | Focus Studio selector |)" [//]: # "(| `u` | Focus Studio selector |)"
[//]: # "(| `p` | Focus Performers selector |)" [//]: # "(| `p` | Focus Performers selector |)"
[//]: # "(| `v` | Focus Movies selector |)" [//]: # "(| `v` | Focus Groups selector |)"
[//]: # "(| `t` | Focus Tags selector |)" [//]: # "(| `t` | Focus Tags selector |)"
## Movies Page shortcuts ## Groups Page shortcuts
| Keyboard sequence | Action | | Keyboard sequence | Action |
|-------------------|--------| |-------------------|--------|
| `n` | New Movie | | `n` | New Group |
## Movie Page shortcuts ## Group Page shortcuts
| Keyboard sequence | Action | | Keyboard sequence | Action |
|-------------------|--------| |-------------------|--------|
| `e` | Edit Movie | | `e` | Edit Group |
| `s s` | Save Movie | | `s s` | Save Group |
| `d d` | Delete Movie | | `d d` | Delete Group |
| `r {1-5}` | [Edit mode] Set rating (stars) | | `r {1-5}` | [Edit mode] Set rating (stars) |
| `r 0` | [Edit mode] Unset rating (stars) | | `r 0` | [Edit mode] Unset rating (stars) |
| `r {0-9} {0-9}` | [Edit mode] Set rating (decimal - `r 0 0` for `10.0`) | | `r {0-9} {0-9}` | [Edit mode] Set rating (decimal - `r 0 0` for `10.0`) |
| ``r ` `` | [Edit mode] Unset rating (decimal) | | ``r ` `` | [Edit mode] Unset rating (decimal) |
| `,` | Expand/Collapse Details | | `,` | Expand/Collapse Details |
| `Ctrl + v` | Paste Movie image | | `Ctrl + v` | Paste Group image |
[//]: # "Commented until implementation is dealt with" [//]: # "Commented until implementation is dealt with"
[//]: # "(| `u` | Focus Studio selector (in edit mode) |)" [//]: # "(| `u` | Focus Studio selector (in edit mode) |)"

View file

@ -248,7 +248,7 @@ The following object types are supported:
* `SceneMarker` * `SceneMarker`
* `Image` * `Image`
* `Gallery` * `Gallery`
* `Movie` * `Group`
* `Performer` * `Performer`
* `Studio` * `Studio`
* `Tag` * `Tag`
@ -296,7 +296,7 @@ For example, here is the `args` values for a Scene update operation:
"studio_id":null, "studio_id":null,
"gallery_ids":null, "gallery_ids":null,
"performer_ids":null, "performer_ids":null,
"movies":null, "groups":null,
"tag_ids":["21"], "tag_ids":["21"],
"cover_image":null, "cover_image":null,
"stash_ids":null "stash_ids":null

View file

@ -15,7 +15,7 @@ Stash supports scraping of metadata from various external sources.
| | Fragment | Search | URL | | | Fragment | Search | URL |
|---|:---:|:---:|:---:| |---|:---:|:---:|:---:|
| gallery | ✔️ | | ✔️ | | gallery | ✔️ | | ✔️ |
| movie | | | ✔️ | | group | | | ✔️ |
| performer | | ✔️ | ✔️ | | performer | | ✔️ | ✔️ |
| scene | ✔️ | ✔️ | ✔️ | | scene | ✔️ | ✔️ | ✔️ |
@ -94,7 +94,7 @@ When used in combination with stash-box, the user can optionally submit scene fi
| | Has Tagger | Source Selection | | | Has Tagger | Source Selection |
|---|:---:|:---:| |---|:---:|:---:|
| gallery | | | | gallery | | |
| movie | | | | group | | |
| performer | ✔️ | | | performer | ✔️ | |
| scene | ✔️ | ✔️ | | scene | ✔️ | ✔️ |

View file

@ -153,9 +153,9 @@ Returns `void`.
- `Icon` - `Icon`
- `ImageDetailPanel` - `ImageDetailPanel`
- `ModalSetting` - `ModalSetting`
- `MovieIDSelect` - `GroupIDSelect`
- `MovieSelect` - `GroupSelect`
- `MovieSelect.sort` - `GroupSelect.sort`
- `NumberSetting` - `NumberSetting`
- `PerformerDetailsPanel` - `PerformerDetailsPanel`
- `PerformerDetailsPanel.DetailGroup` - `PerformerDetailsPanel.DetailGroup`

View file

@ -48,6 +48,7 @@ export function renderLabel(options: {
// the mouse wheel will change the field value _and_ scroll the window. // the mouse wheel will change the field value _and_ scroll the window.
// This hook prevents the propagation that causes the window to scroll. // This hook prevents the propagation that causes the window to scroll.
export function useStopWheelScroll(ref: React.RefObject<HTMLElement>) { export function useStopWheelScroll(ref: React.RefObject<HTMLElement>) {
// removed the dependency array because the underlying ref value may change
useEffect(() => { useEffect(() => {
const { current } = ref; const { current } = ref;
@ -66,17 +67,18 @@ export function useStopWheelScroll(ref: React.RefObject<HTMLElement>) {
current.removeEventListener("wheel", stopWheelScroll); current.removeEventListener("wheel", stopWheelScroll);
} }
}; };
}, [ref]); });
} }
const InputField: React.FC< // NumberField is a wrapper around Form.Control that prevents wheel events from scrolling the window.
export const NumberField: React.FC<
InputHTMLAttributes<HTMLInputElement> & FormControlProps InputHTMLAttributes<HTMLInputElement> & FormControlProps
> = (props) => { > = (props) => {
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
useStopWheelScroll(inputRef); useStopWheelScroll(inputRef);
return <Form.Control {...props} ref={inputRef} />; return <Form.Control {...props} type="number" ref={inputRef} />;
}; };
type Formik<V extends FormikValues> = ReturnType<typeof useFormik<V>>; type Formik<V extends FormikValues> = ReturnType<typeof useFormik<V>>;
@ -134,9 +136,18 @@ export function formikUtils<V extends FormikValues>(
isInvalid={!!error} isInvalid={!!error}
/> />
); );
} else if (type === "number") {
<NumberField
type={type}
className="text-input"
placeholder={placeholder}
{...formikProps}
value={value}
isInvalid={!!error}
/>;
} else { } else {
control = ( control = (
<InputField <Form.Control
type={type} type={type}
className="text-input" className="text-input"
placeholder={placeholder} placeholder={placeholder}