mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 16:59:12 +00:00
feat: improve release parsing and filtering (#257)
* feat(releases): improve parsing * refactor: extend filtering add more tests * feat: improve macro * feat: add and remove fields * feat: add freeleech percent to bonus * feat: filter by origin
This commit is contained in:
parent
bb62e724a1
commit
e6c151a029
26 changed files with 3210 additions and 3201 deletions
|
@ -14,7 +14,7 @@
|
|||
"react-cookie": "^4.1.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-hot-toast": "^2.1.1",
|
||||
"react-multi-select-component": "^4.0.2",
|
||||
"react-multi-select-component": "4.2.5",
|
||||
"react-query": "^3.18.1",
|
||||
"react-ridge-state": "4.2.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
|
@ -124,4 +124,4 @@
|
|||
},
|
||||
"globals": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,10 +8,11 @@ import { classNames, COL_WIDTHS } from "../../utils";
|
|||
import { SettingsContext } from "../../utils/Context";
|
||||
|
||||
interface MultiSelectProps {
|
||||
name: string;
|
||||
label?: string;
|
||||
options?: [] | any;
|
||||
name: string;
|
||||
columns?: COL_WIDTHS;
|
||||
creatable?: boolean;
|
||||
}
|
||||
|
||||
export const MultiSelect = ({
|
||||
|
@ -19,8 +20,16 @@ export const MultiSelect = ({
|
|||
label,
|
||||
options,
|
||||
columns,
|
||||
creatable,
|
||||
}: MultiSelectProps) => {
|
||||
const settingsContext = SettingsContext.useValue();
|
||||
|
||||
const handleNewField = (value: string) => ({
|
||||
value: value.toUpperCase(),
|
||||
label: value.toUpperCase(),
|
||||
key: value,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
|
@ -42,11 +51,17 @@ export const MultiSelect = ({
|
|||
<RMSC
|
||||
{...field}
|
||||
type="select"
|
||||
options={options}
|
||||
options={[...[...options, ...field.value.map((i: any) => ({ value: i.value ?? i, label: i.label ?? i}))].reduce((map, obj) => map.set(obj.value, obj), new Map()).values()]}
|
||||
labelledBy={name}
|
||||
value={field.value && field.value.map((item: any) => options.find((o: any) => o.value === item))}
|
||||
isCreatable={creatable}
|
||||
onCreateOption={handleNewField}
|
||||
value={field.value && field.value.map((item: any) => ({
|
||||
value: item.value ? item.value : item,
|
||||
label: item.label ? item.label : item,
|
||||
}))}
|
||||
onChange={(values: any) => {
|
||||
const am = values && values.map((i: any) => i.value);
|
||||
|
||||
setFieldValue(field.name, am);
|
||||
}}
|
||||
className={settingsContext.darkTheme ? "dark" : ""}
|
||||
|
|
|
@ -12,29 +12,25 @@ export const resolutions = [
|
|||
export const RESOLUTION_OPTIONS = resolutions.map(r => ({ value: r, label: r, key: r}));
|
||||
|
||||
export const codecs = [
|
||||
"AVC",
|
||||
"Remux",
|
||||
"h.264 Remux",
|
||||
"h.265 Remux",
|
||||
"HEVC",
|
||||
"VC-1",
|
||||
"VC-1 Remux",
|
||||
"h264",
|
||||
"h265",
|
||||
"H.264",
|
||||
"H.265",
|
||||
"x264",
|
||||
"x265",
|
||||
"h264 10-bit",
|
||||
"h265 10-bit",
|
||||
"x264 10-bit",
|
||||
"x265 10-bit",
|
||||
"AVC",
|
||||
"VC-1",
|
||||
"AV1",
|
||||
"XviD"
|
||||
];
|
||||
|
||||
export const CODECS_OPTIONS = codecs.map(v => ({ value: v, label: v, key: v}));
|
||||
|
||||
export const sources = [
|
||||
"WEB-DL",
|
||||
"BluRay",
|
||||
"UHD.BluRay",
|
||||
"WEB-DL",
|
||||
"WEB",
|
||||
"WEBRip",
|
||||
"BD5",
|
||||
"BD9",
|
||||
"BDr",
|
||||
|
@ -51,7 +47,6 @@ export const sources = [
|
|||
"HDTV",
|
||||
"Mixed",
|
||||
"SiteRip",
|
||||
"Webrip",
|
||||
];
|
||||
|
||||
export const SOURCES_OPTIONS = sources.map(v => ({ value: v, label: v, key: v}));
|
||||
|
@ -68,6 +63,7 @@ export const hdr = [
|
|||
"HDR",
|
||||
"HDR10",
|
||||
"HDR10+",
|
||||
"HLG",
|
||||
"DV",
|
||||
"DV HDR",
|
||||
"DV HDR10",
|
||||
|
@ -78,6 +74,13 @@ export const hdr = [
|
|||
|
||||
export const HDR_OPTIONS = hdr.map(v => ({ value: v, label: v, key: v}));
|
||||
|
||||
export const quality_other = [
|
||||
"REMUX",
|
||||
"HYBRID",
|
||||
"REPACK",
|
||||
];
|
||||
|
||||
export const OTHER_OPTIONS = quality_other.map(v => ({ value: v, label: v, key: v}));
|
||||
|
||||
export const formatMusic = [
|
||||
"MP3",
|
||||
|
@ -135,11 +138,20 @@ export const releaseTypeMusic = [
|
|||
"Demo",
|
||||
"Concert Recording",
|
||||
"DJ Mix",
|
||||
"Unkown",
|
||||
"Unknown",
|
||||
];
|
||||
|
||||
export const RELEASE_TYPE_MUSIC_OPTIONS = releaseTypeMusic.map(v => ({ value: v, label: v, key: v}));
|
||||
|
||||
export const originOptions = [
|
||||
"P2P",
|
||||
"Internal",
|
||||
"SCENE",
|
||||
"O-SCENE",
|
||||
];
|
||||
|
||||
export const ORIGIN_OPTIONS = originOptions.map(v => ({ value: v, label: v, key: v}));
|
||||
|
||||
export interface RadioFieldsetOption {
|
||||
label: string;
|
||||
description: string;
|
||||
|
|
|
@ -26,7 +26,7 @@ import {
|
|||
FORMATS_OPTIONS,
|
||||
SOURCES_MUSIC_OPTIONS,
|
||||
QUALITY_MUSIC_OPTIONS,
|
||||
RELEASE_TYPE_MUSIC_OPTIONS
|
||||
RELEASE_TYPE_MUSIC_OPTIONS, OTHER_OPTIONS, ORIGIN_OPTIONS
|
||||
} from "../../domain/constants";
|
||||
import { queryClient } from "../../App";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
|
@ -264,6 +264,8 @@ export default function FilterDetails() {
|
|||
containers: filter.containers || [],
|
||||
match_hdr: filter.match_hdr || [],
|
||||
except_hdr: filter.except_hdr || [],
|
||||
match_other: filter.match_other || [],
|
||||
except_other: filter.except_other || [],
|
||||
seasons: filter.seasons,
|
||||
episodes: filter.episodes,
|
||||
match_releases: filter.match_releases,
|
||||
|
@ -288,6 +290,7 @@ export default function FilterDetails() {
|
|||
perfect_flac: filter.perfect_flac,
|
||||
artists: filter.artists,
|
||||
albums: filter.albums,
|
||||
origins: filter.origins || [],
|
||||
indexers: filter.indexers || [],
|
||||
actions: filter.actions || [],
|
||||
} as Filter}
|
||||
|
@ -403,18 +406,23 @@ function MoviesTv() {
|
|||
<TitleSubtitle title="Quality" subtitle="Set resolution, source, codec and related match constraints" />
|
||||
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<MultiSelect name="resolutions" options={RESOLUTION_OPTIONS} label="resolutions" columns={6} />
|
||||
<MultiSelect name="sources" options={SOURCES_OPTIONS} label="sources" columns={6} />
|
||||
<MultiSelect name="resolutions" options={RESOLUTION_OPTIONS} label="resolutions" columns={6} creatable={true} />
|
||||
<MultiSelect name="sources" options={SOURCES_OPTIONS} label="sources" columns={6} creatable={true} />
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<MultiSelect name="codecs" options={CODECS_OPTIONS} label="codecs" columns={6} />
|
||||
<MultiSelect name="containers" options={CONTAINER_OPTIONS} label="containers" columns={6} />
|
||||
<MultiSelect name="codecs" options={CODECS_OPTIONS} label="codecs" columns={6} creatable={true} />
|
||||
<MultiSelect name="containers" options={CONTAINER_OPTIONS} label="containers" columns={6} creatable={true} />
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<MultiSelect name="match_hdr" options={HDR_OPTIONS} label="Match HDR" columns={6} />
|
||||
<MultiSelect name="except_hdr" options={HDR_OPTIONS} label="Except HDR" columns={6} />
|
||||
<MultiSelect name="match_hdr" options={HDR_OPTIONS} label="Match HDR" columns={6} creatable={true} />
|
||||
<MultiSelect name="except_hdr" options={HDR_OPTIONS} label="Except HDR" columns={6} creatable={true} />
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<MultiSelect name="match_other" options={OTHER_OPTIONS} label="Match Other" columns={6} creatable={true} />
|
||||
<MultiSelect name="except_other" options={OTHER_OPTIONS} label="Except Other" columns={6} creatable={true} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -474,134 +482,76 @@ function Music() {
|
|||
}
|
||||
|
||||
function Advanced() {
|
||||
const [releasesIsOpen, toggleReleases] = useToggle(false)
|
||||
const [groupsIsOpen, toggleGroups] = useToggle(false)
|
||||
const [categoriesIsOpen, toggleCategories] = useToggle(false)
|
||||
const [uploadersIsOpen, toggleUploaders] = useToggle(false)
|
||||
const [freeleechIsOpen, toggleFreeleech] = useToggle(false)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mt-6 lg:pb-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex justify-between items-center cursor-pointer" onClick={toggleReleases}>
|
||||
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
||||
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Releases</h3>
|
||||
<p className="ml-2 mt-1 text-sm text-gray-500 dark:text-gray-400 truncate">Match only certain release names and/or ignore other release names</p>
|
||||
</div>
|
||||
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center px-4 py-2 border-transparent text-sm font-medium text-white"
|
||||
>
|
||||
{releasesIsOpen ? <ChevronDownIcon className="h-6 w-6 text-gray-500" aria-hidden="true" /> : <ChevronRightIcon className="h-6 w-6 text-gray-500" aria-hidden="true" />}
|
||||
</button>
|
||||
</div>
|
||||
<CollapsableSection title="Releases" subtitle="Match only certain release names and/or ignore other release names">
|
||||
<TextField name="match_releases" label="Match releases" columns={6} placeholder="eg. *some?movie*,*some?show*s01*" />
|
||||
<TextField name="except_releases" label="Except releases" columns={6} placeholder="" />
|
||||
</CollapsableSection>
|
||||
|
||||
<CollapsableSection title="Groups" subtitle="Match only certain groups and/or ignore other groups">
|
||||
<TextField name="match_release_groups" label="Match release groups" columns={6} placeholder="eg. group1,group2" />
|
||||
<TextField name="except_release_groups" label="Except release groups" columns={6} placeholder="eg. badgroup1,badgroup2" />
|
||||
</CollapsableSection>
|
||||
|
||||
<CollapsableSection title="Categories and tags" subtitle="Match or ignore categories or tags">
|
||||
<TextField name="match_categories" label="Match categories" columns={6} placeholder="eg. *category*,category1" />
|
||||
<TextField name="except_categories" label="Except categories" columns={6} placeholder="eg. *category*" />
|
||||
|
||||
<TextField name="tags" label="Match tags" columns={6} placeholder="eg. tag1,tag2" />
|
||||
<TextField name="except_tags" label="Except tags" columns={6} placeholder="eg. tag1,tag2" />
|
||||
</CollapsableSection>
|
||||
|
||||
<CollapsableSection title="Uploaders" subtitle="Match or ignore uploaders">
|
||||
<TextField name="match_uploaders" label="Match uploaders" columns={6} placeholder="eg. uploader1" />
|
||||
<TextField name="except_uploaders" label="Except uploaders" columns={6} placeholder="eg. anonymous" />
|
||||
</CollapsableSection>
|
||||
|
||||
<CollapsableSection title="Origins" subtitle="Match Internals, scene, p2p etc if announced">
|
||||
<MultiSelect name="origins" options={ORIGIN_OPTIONS} label="Origins" columns={6} />
|
||||
</CollapsableSection>
|
||||
|
||||
<CollapsableSection title="Freeleech" subtitle="Match only freeleech and freeleech percent">
|
||||
<div className="col-span-6">
|
||||
<SwitchGroup name="freeleech" label="Freeleech" />
|
||||
</div>
|
||||
{releasesIsOpen && (
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<TextField name="match_releases" label="Match releases" columns={6} placeholder="eg. *some?movie*,*some?show*s01*" />
|
||||
<TextField name="except_releases" label="Except releases" columns={6} placeholder="" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 lg:pb-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex justify-between items-center cursor-pointer" onClick={toggleGroups}>
|
||||
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
||||
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Groups</h3>
|
||||
<p className="ml-2 mt-1 text-sm text-gray-500 dark:text-gray-400 truncate">Match only certain groups and/or ignore other groups</p>
|
||||
</div>
|
||||
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center px-4 py-2 border-transparent text-sm font-medium text-white"
|
||||
>
|
||||
{groupsIsOpen ? <ChevronDownIcon className="h-6 w-6 text-gray-500" aria-hidden="true" /> : <ChevronRightIcon className="h-6 w-6 text-gray-500" aria-hidden="true" />}
|
||||
</button>
|
||||
</div>
|
||||
<TextField name="freeleech_percent" label="Freeleech percent" columns={6} />
|
||||
</CollapsableSection>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface CollapsableSectionProps {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
children: any;
|
||||
}
|
||||
|
||||
function CollapsableSection({ title, subtitle, children }: CollapsableSectionProps) {
|
||||
const [isOpen, toggleOpen] = useToggle(false)
|
||||
|
||||
return(
|
||||
<div className="mt-6 lg:pb-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex justify-between items-center cursor-pointer" onClick={toggleOpen}>
|
||||
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
||||
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">{title}</h3>
|
||||
<p className="ml-2 mt-1 text-sm text-gray-500 dark:text-gray-400 truncate">{subtitle}</p>
|
||||
</div>
|
||||
{groupsIsOpen && (
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<TextField name="match_release_groups" label="Match release groups" columns={6} placeholder="eg. group1,group2" />
|
||||
<TextField name="except_release_groups" label="Except release groups" columns={6} placeholder="eg. badgroup1,badgroup2" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 lg:pb-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex justify-between items-center cursor-pointer" onClick={toggleCategories}>
|
||||
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
||||
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Categories and tags</h3>
|
||||
<p className="ml-2 mt-1 text-sm text-gray-500 dark:text-gray-400 truncate">Match or ignore categories or tags</p>
|
||||
</div>
|
||||
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center px-4 py-2 border-transparent text-sm font-medium text-white"
|
||||
>
|
||||
{categoriesIsOpen ? <ChevronDownIcon className="h-6 w-6 text-gray-500" aria-hidden="true" /> : <ChevronRightIcon className="h-6 w-6 text-gray-500" aria-hidden="true" />}
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center px-4 py-2 border-transparent text-sm font-medium text-white"
|
||||
>
|
||||
{isOpen ? <ChevronDownIcon className="h-6 w-6 text-gray-500" aria-hidden="true" /> : <ChevronRightIcon className="h-6 w-6 text-gray-500" aria-hidden="true" />}
|
||||
</button>
|
||||
</div>
|
||||
{categoriesIsOpen && (
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<TextField name="match_categories" label="Match categories" columns={6} placeholder="eg. *category*,category1" />
|
||||
<TextField name="except_categories" label="Except categories" columns={6} placeholder="eg. *category*" />
|
||||
|
||||
<TextField name="tags" label="Match tags" columns={6} placeholder="eg. tag1,tag2" />
|
||||
<TextField name="except_tags" label="Except tags" columns={6} placeholder="eg. tag1,tag2" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 lg:pb-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex justify-between items-center cursor-pointer" onClick={toggleUploaders}>
|
||||
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
||||
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Uploaders</h3>
|
||||
<p className="ml-2 mt-1 text-sm text-gray-500 dark:text-gray-400 truncate">Match or ignore uploaders</p>
|
||||
</div>
|
||||
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center px-4 py-2 border-transparent text-sm font-medium text-white"
|
||||
>
|
||||
{uploadersIsOpen ? <ChevronDownIcon className="h-6 w-6 text-gray-500" aria-hidden="true" /> : <ChevronRightIcon className="h-6 w-6 text-gray-500" aria-hidden="true" />}
|
||||
</button>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
{children}
|
||||
</div>
|
||||
{uploadersIsOpen && (
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<TextField name="match_uploaders" label="Match uploaders" columns={6} placeholder="eg. uploader1" />
|
||||
<TextField name="except_uploaders" label="Except uploaders" columns={6} placeholder="eg. anonymous" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 lg:pb-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex justify-between items-center cursor-pointer" onClick={toggleFreeleech}>
|
||||
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
||||
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Freeleech</h3>
|
||||
<p className="ml-2 mt-1 text-sm text-gray-500 dark:text-gray-400 truncate">Match only freeleech and freeleech percent</p>
|
||||
</div>
|
||||
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center px-4 py-2 border-transparent text-sm font-medium text-white"
|
||||
>
|
||||
{freeleechIsOpen ? <ChevronDownIcon className="h-6 w-6 text-gray-500" aria-hidden="true" /> : <ChevronRightIcon className="h-6 w-6 text-gray-500" aria-hidden="true" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{freeleechIsOpen && (
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<div className="col-span-6">
|
||||
<SwitchGroup name="freeleech" label="Freeleech" />
|
||||
</div>
|
||||
|
||||
<TextField name="freeleech_percent" label="Freeleech percent" columns={6} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
4
web/src/types/Filter.d.ts
vendored
4
web/src/types/Filter.d.ts
vendored
|
@ -14,7 +14,7 @@ interface Filter {
|
|||
match_release_groups: string;
|
||||
except_release_groups: string;
|
||||
scene: boolean;
|
||||
origins: string;
|
||||
origins: string[];
|
||||
freeleech: boolean;
|
||||
freeleech_percent: string;
|
||||
shows: string;
|
||||
|
@ -26,6 +26,8 @@ interface Filter {
|
|||
containers: string[];
|
||||
match_hdr: string[];
|
||||
except_hdr: string[];
|
||||
match_other: string[];
|
||||
except_other: string[];
|
||||
years: string;
|
||||
artists: string;
|
||||
albums: string;
|
||||
|
|
|
@ -7334,10 +7334,10 @@ react-is@^17.0.1:
|
|||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||
|
||||
react-multi-select-component@^4.0.2:
|
||||
version "4.2.4"
|
||||
resolved "https://registry.yarnpkg.com/react-multi-select-component/-/react-multi-select-component-4.2.4.tgz#4eb11f0c1b0d94b05738b21f1c09a4a6ec8089c6"
|
||||
integrity sha512-HhXV3lLi5k2FCGuVUsM8KoFLPCGhb2JAz3HWS8jg7IY1LKr+5/W54+0w7MlsyjeMS0r+vg4CYnv315dn3B20IA==
|
||||
react-multi-select-component@4.2.5:
|
||||
version "4.2.5"
|
||||
resolved "https://registry.yarnpkg.com/react-multi-select-component/-/react-multi-select-component-4.2.5.tgz#507a0814baa856bfbd98e48a854f14e6b4d0f0d8"
|
||||
integrity sha512-/rfyCqp+Q01BSDlzfkF8PWpuAxhIf7650CnW8xD01+NC5nEUj8/JcFL3MgMlM8bpYqrSiyMX/v3hKZ9nDNsZdA==
|
||||
|
||||
react-query@^3.18.1:
|
||||
version "3.34.19"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue