mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-18 21:35:27 -04:00
Compare commits
113 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e9ef57e3d | |||
| 59f3be0813 | |||
| fb540040ef | |||
| b8af3af9f1 | |||
| 78cf13d341 | |||
| 978349e241 | |||
| a77bf64352 | |||
| 832de3e75e | |||
| 8d4ba77b12 | |||
| 409823c7e8 | |||
| 8e636d7a37 | |||
| 38c0135d7c | |||
| 22005dc8c5 | |||
| 73208e2f60 | |||
| 1df0ba9e5a | |||
| 020ed32fcf | |||
| 3ddc6ac6de | |||
| 0f225b05c0 | |||
| e006b40532 | |||
| e88f25d3bf | |||
| 1fcfb88d2a | |||
| 804eaa1227 | |||
| c41e3ce1e3 | |||
| 682d2b4e1b | |||
| c114e2ddb7 | |||
| f8a879f4c1 | |||
| 33139d4b53 | |||
| de69d8ec7e | |||
| 03b9c957b8 | |||
| 41ddacc395 | |||
| 8a558b379a | |||
| 240a0339be | |||
| ff724b7f40 | |||
| fcf68d9259 | |||
| 404e6d68ea | |||
| df672487cf | |||
| 0bc4903954 | |||
| 10b55bbee6 | |||
| 20ef22be94 | |||
| 57534db2f8 | |||
| 1e89a1a3cb | |||
| f502eaffe3 | |||
| fe40d83aa4 | |||
| 07374de747 | |||
| 135b5c2ddd | |||
| 0784f56b9a | |||
| 562e0dd7c0 | |||
| 28599f87af | |||
| 86446a7686 | |||
| 2f1793d87a | |||
| a641f2897a | |||
| 32fa63d24d | |||
| ebfa000375 | |||
| 39074b0b1d | |||
| 354ed96572 | |||
| c8f419b014 | |||
| a001216957 | |||
| a6735e7a3f | |||
| ea0bfed700 | |||
| 620220b269 | |||
| c435fcd685 | |||
| 3828e475cc | |||
| e6e1078c15 | |||
| 6660db22ec | |||
| bc0fc623ee | |||
| da610a1f40 | |||
| 6d0f10b877 | |||
| 4f0e1c54c1 | |||
| 2f0ca42341 | |||
| 768af433d1 | |||
| 8bf0298227 | |||
| a7cb264cc8 | |||
| 10302323af | |||
| dc1524c64f | |||
| 4d7a3d0909 | |||
| 30a52d11aa | |||
| be4a9e9491 | |||
| e196c1be69 | |||
| 106ffd410c | |||
| c199fd05d3 | |||
| 75fae9262c | |||
| faf9173b3b | |||
| 0fa8e24f48 | |||
| 27da041388 | |||
| ca38a9b577 | |||
| 4b72a0a4e8 | |||
| 9875e550a8 | |||
| c9aa59340c | |||
| 30c36fdc3b | |||
| 3976e5daf7 | |||
| fca8c36156 | |||
| 85f53e8cb1 | |||
| a73a5cc85c | |||
| 89d730cdfd | |||
| 99fc52039f | |||
| e6bd58453a | |||
| 9603f0b086 | |||
| d84c450094 | |||
| 97ebaf2796 | |||
| 31bf9e313e | |||
| 6cccacd4d7 | |||
| 3c857135c5 | |||
| 750a9353f8 | |||
| 71a19377d9 | |||
| 4b5ff3927d | |||
| 4d8a443681 | |||
| 6a332b40ac | |||
| a929548ae3 | |||
| 55363f4e3d | |||
| f20ac9dc34 | |||
| 8b20a9449c | |||
| 24f03fc1e9 | |||
| 5513d7bc5d |
@@ -22,7 +22,7 @@ env:
|
|||||||
FRAMEWORK: net6.0
|
FRAMEWORK: net6.0
|
||||||
RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||||
SONARR_MAJOR_VERSION: 4
|
SONARR_MAJOR_VERSION: 4
|
||||||
VERSION: 4.0.9
|
VERSION: 4.0.10
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
backend:
|
backend:
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
name: 'Support Requests'
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [labeled, unlabeled, reopened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
action:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.repository == 'Sonarr/Sonarr'
|
||||||
|
steps:
|
||||||
|
- uses: dessant/support-requests@v4
|
||||||
|
with:
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
support-label: 'support'
|
||||||
|
issue-comment: >
|
||||||
|
:wave: @{issue-author}, we use the issue tracker exclusively
|
||||||
|
for bug reports and feature requests. However, this issue appears
|
||||||
|
to be a support request. Please use one of the support channels:
|
||||||
|
[forums](https://forums.sonarr.tv/), [subreddit](https://www.reddit.com/r/sonarr/),
|
||||||
|
[discord](https://discord.gg/Ex7FmFK), or [IRC](https://web.libera.chat/?channels=#sonarr)
|
||||||
|
for support/questions.
|
||||||
|
close-issue: true
|
||||||
|
issue-close-reason: 'not planned'
|
||||||
|
lock-issue: false
|
||||||
|
issue-lock-reason: 'off-topic'
|
||||||
@@ -162,3 +162,6 @@ src/.idea/
|
|||||||
|
|
||||||
# API doc generation
|
# API doc generation
|
||||||
.config/
|
.config/
|
||||||
|
|
||||||
|
# Ignore Jetbrains IntelliJ Workspace Directories
|
||||||
|
.idea/
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
width="70px" height="70px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-1.3318" y1="43.7371" x2="67.0419" y2="26.0967">
|
|
||||||
<stop offset="0.1237" style="stop-color:#7866FF"/>
|
|
||||||
<stop offset="0.5376" style="stop-color:#FE2EB6"/>
|
|
||||||
<stop offset="0.8548" style="stop-color:#FD0486"/>
|
|
||||||
</linearGradient>
|
|
||||||
<polygon style="fill:url(#SVGID_1_);" points="67.3,16 43.7,0 0,31.1 11.1,70 58.9,60.3 "/>
|
|
||||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="45.9148" y1="38.9098" x2="67.6577" y2="9.0989">
|
|
||||||
<stop offset="0.1237" style="stop-color:#FF0080"/>
|
|
||||||
<stop offset="0.2587" style="stop-color:#FE0385"/>
|
|
||||||
<stop offset="0.4109" style="stop-color:#FA0C92"/>
|
|
||||||
<stop offset="0.5713" style="stop-color:#F41BA9"/>
|
|
||||||
<stop offset="0.7363" style="stop-color:#EB2FC8"/>
|
|
||||||
<stop offset="0.8656" style="stop-color:#E343E6"/>
|
|
||||||
</linearGradient>
|
|
||||||
<polygon style="fill:url(#SVGID_2_);" points="67.3,16 43.7,0 38,15.7 38,47.8 70,47.8 "/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<rect x="13.4" y="13.4" style="fill:#000000;" width="43.2" height="43.2"/>
|
|
||||||
<rect x="17.4" y="48.5" style="fill:#FFFFFF;" width="16.2" height="2.7"/>
|
|
||||||
<g>
|
|
||||||
<path style="fill:#FFFFFF;" d="M17.4,19.1h6.9c5.6,0,9.5,3.8,9.5,8.9V28c0,5-3.9,8.9-9.5,8.9h-6.9V19.1z M21.4,22.7v10.7h3
|
|
||||||
c3.2,0,5.4-2.2,5.4-5.3V28c0-3.2-2.2-5.4-5.4-5.4H21.4z"/>
|
|
||||||
<polygon style="fill:#FFFFFF;" points="40.3,22.7 34.9,22.7 34.9,19.1 49.6,19.1 49.6,22.7 44.2,22.7 44.2,37 40.3,37 "/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,66 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
width="120.1px" height="130.2px" viewBox="0 0 120.1 130.2" style="enable-background:new 0 0 120.1 130.2;" xml:space="preserve"
|
|
||||||
>
|
|
||||||
<g>
|
|
||||||
<linearGradient id="XMLID_2_" gradientUnits="userSpaceOnUse" x1="31.8412" y1="120.5578" x2="110.2402" y2="73.24">
|
|
||||||
<stop offset="0" style="stop-color:#FCEE39"/>
|
|
||||||
<stop offset="1" style="stop-color:#F37B3D"/>
|
|
||||||
</linearGradient>
|
|
||||||
<path id="XMLID_3041_" style="fill:url(#XMLID_2_);" d="M118.6,71.8c0.9-0.8,1.4-1.9,1.5-3.2c0.1-2.6-1.8-4.7-4.4-4.9
|
|
||||||
c-1.2-0.1-2.4,0.4-3.3,1.1l0,0l-83.8,45.9c-1.9,0.8-3.6,2.2-4.7,4.1c-2.9,4.8-1.3,11,3.6,13.9c3.4,2,7.5,1.8,10.7-0.2l0,0l0,0
|
|
||||||
c0.2-0.2,0.5-0.3,0.7-0.5l78-54.8C117.3,72.9,118.4,72.1,118.6,71.8L118.6,71.8L118.6,71.8z"/>
|
|
||||||
<linearGradient id="XMLID_3_" gradientUnits="userSpaceOnUse" x1="48.3607" y1="6.9083" x2="119.9179" y2="69.5546">
|
|
||||||
<stop offset="0" style="stop-color:#EF5A6B"/>
|
|
||||||
<stop offset="0.57" style="stop-color:#F26F4E"/>
|
|
||||||
<stop offset="1" style="stop-color:#F37B3D"/>
|
|
||||||
</linearGradient>
|
|
||||||
<path id="XMLID_3049_" style="fill:url(#XMLID_3_);" d="M118.8,65.1L118.8,65.1L55,2.5C53.6,1,51.6,0,49.3,0
|
|
||||||
c-4.3,0-7.7,3.5-7.7,7.7v0c0,2.1,0.8,3.9,2.1,5.3l0,0l0,0c0.4,0.4,0.8,0.7,1.2,1l67.4,57.7l0,0c0.8,0.7,1.8,1.2,3,1.3
|
|
||||||
c2.6,0.1,4.7-1.8,4.9-4.4C120.2,67.3,119.7,66,118.8,65.1z"/>
|
|
||||||
<linearGradient id="XMLID_4_" gradientUnits="userSpaceOnUse" x1="52.9467" y1="63.6407" x2="10.5379" y2="37.1562">
|
|
||||||
<stop offset="0" style="stop-color:#7C59A4"/>
|
|
||||||
<stop offset="0.3852" style="stop-color:#AF4C92"/>
|
|
||||||
<stop offset="0.7654" style="stop-color:#DC4183"/>
|
|
||||||
<stop offset="0.957" style="stop-color:#ED3D7D"/>
|
|
||||||
</linearGradient>
|
|
||||||
<path id="XMLID_3042_" style="fill:url(#XMLID_4_);" d="M57.1,59.5C57,59.5,17.7,28.5,16.9,28l0,0l0,0c-0.6-0.3-1.2-0.6-1.8-0.9
|
|
||||||
c-5.8-2.2-12.2,0.8-14.4,6.6c-1.9,5.1,0.2,10.7,4.6,13.4l0,0l0,0C6,47.5,6.6,47.8,7.3,48c0.4,0.2,45.4,18.8,45.4,18.8l0,0
|
|
||||||
c1.8,0.8,3.9,0.3,5.1-1.2C59.3,63.7,59,61,57.1,59.5z"/>
|
|
||||||
<linearGradient id="XMLID_5_" gradientUnits="userSpaceOnUse" x1="52.1736" y1="3.7019" x2="10.7706" y2="37.8971">
|
|
||||||
<stop offset="0" style="stop-color:#EF5A6B"/>
|
|
||||||
<stop offset="0.364" style="stop-color:#EE4E72"/>
|
|
||||||
<stop offset="1" style="stop-color:#ED3D7D"/>
|
|
||||||
</linearGradient>
|
|
||||||
<path id="XMLID_3057_" style="fill:url(#XMLID_5_);" d="M49.3,0c-1.7,0-3.3,0.6-4.6,1.5L4.9,28.3c-0.1,0.1-0.2,0.1-0.2,0.2l-0.1,0
|
|
||||||
l0,0c-1.7,1.2-3.1,3-3.9,5.1C-1.5,39.4,1.5,45.9,7.3,48c3.6,1.4,7.5,0.7,10.4-1.4l0,0l0,0c0.7-0.5,1.3-1,1.8-1.6l34.6-31.2l0,0
|
|
||||||
c1.8-1.4,3-3.6,3-6.1v0C57.1,3.5,53.6,0,49.3,0z"/>
|
|
||||||
<g id="XMLID_3008_">
|
|
||||||
<rect id="XMLID_3033_" x="34.6" y="37.4" style="fill:#000000;" width="51" height="51"/>
|
|
||||||
<rect id="XMLID_3032_" x="39" y="78.8" style="fill:#FFFFFF;" width="19.1" height="3.2"/>
|
|
||||||
<g id="XMLID_3009_">
|
|
||||||
<path id="XMLID_3030_" style="fill:#FFFFFF;" d="M38.8,50.8l1.5-1.4c0.4,0.5,0.8,0.8,1.3,0.8c0.6,0,0.9-0.4,0.9-1.2l0-5.3l2.3,0
|
|
||||||
l0,5.3c0,1-0.3,1.8-0.8,2.3c-0.5,0.5-1.3,0.8-2.3,0.8C40.2,52.2,39.4,51.6,38.8,50.8z"/>
|
|
||||||
<path id="XMLID_3028_" style="fill:#FFFFFF;" d="M45.3,43.8l6.7,0v1.9l-4.4,0V47l4,0l0,1.8l-4,0l0,1.3l4.5,0l0,2l-6.7,0
|
|
||||||
L45.3,43.8z"/>
|
|
||||||
<path id="XMLID_3026_" style="fill:#FFFFFF;" d="M55,45.8l-2.5,0l0-2l7.3,0l0,2l-2.5,0l0,6.3l-2.3,0L55,45.8z"/>
|
|
||||||
<path id="XMLID_3022_" style="fill:#FFFFFF;" d="M39,54l4.3,0c1,0,1.8,0.3,2.3,0.7c0.3,0.3,0.5,0.8,0.5,1.4v0
|
|
||||||
c0,1-0.5,1.5-1.3,1.9c1,0.3,1.6,0.9,1.6,2v0c0,1.4-1.2,2.3-3.1,2.3l-4.3,0L39,54z M43.8,56.6c0-0.5-0.4-0.7-1-0.7l-1.5,0l0,1.5
|
|
||||||
l1.4,0C43.4,57.3,43.8,57.1,43.8,56.6L43.8,56.6z M43,59l-1.8,0l0,1.5H43c0.7,0,1.1-0.3,1.1-0.8v0C44.1,59.2,43.7,59,43,59z"/>
|
|
||||||
<path id="XMLID_3019_" style="fill:#FFFFFF;" d="M46.8,54l3.9,0c1.3,0,2.1,0.3,2.7,0.9c0.5,0.5,0.7,1.1,0.7,1.9v0
|
|
||||||
c0,1.3-0.7,2.1-1.7,2.6l2,2.9l-2.6,0l-1.7-2.5h-1l0,2.5l-2.3,0L46.8,54z M50.6,58c0.8,0,1.2-0.4,1.2-1v0c0-0.7-0.5-1-1.2-1
|
|
||||||
l-1.5,0v2H50.6z"/>
|
|
||||||
<path id="XMLID_3016_" style="fill:#FFFFFF;" d="M56.8,54l2.2,0l3.5,8.4l-2.5,0l-0.6-1.5l-3.2,0l-0.6,1.5l-2.4,0L56.8,54z
|
|
||||||
M58.8,59l-0.9-2.3L57,59L58.8,59z"/>
|
|
||||||
<path id="XMLID_3014_" style="fill:#FFFFFF;" d="M62.8,54l2.3,0l0,8.3l-2.3,0L62.8,54z"/>
|
|
||||||
<path id="XMLID_3012_" style="fill:#FFFFFF;" d="M65.7,54l2.1,0l3.4,4.4l0-4.4l2.3,0l0,8.3l-2,0L68,57.8l0,4.6l-2.3,0L65.7,54z"
|
|
||||||
/>
|
|
||||||
<path id="XMLID_3010_" style="fill:#FFFFFF;" d="M73.7,61.1l1.3-1.5c0.8,0.7,1.7,1,2.7,1c0.6,0,1-0.2,1-0.6v0
|
|
||||||
c0-0.4-0.3-0.5-1.4-0.8c-1.8-0.4-3.1-0.9-3.1-2.6v0c0-1.5,1.2-2.7,3.2-2.7c1.4,0,2.5,0.4,3.4,1.1l-1.2,1.6
|
|
||||||
c-0.8-0.5-1.6-0.8-2.3-0.8c-0.6,0-0.8,0.2-0.8,0.5v0c0,0.4,0.3,0.5,1.4,0.8c1.9,0.4,3.1,1,3.1,2.6v0c0,1.7-1.3,2.7-3.4,2.7
|
|
||||||
C76.1,62.5,74.7,62,73.7,61.1z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.8 KiB |
@@ -1,50 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
width="70px" height="70px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="22.9451" y1="75.7869" x2="74.7868" y2="20.6415">
|
|
||||||
<stop offset="1.612903e-002" style="stop-color:#B35BA3"/>
|
|
||||||
<stop offset="0.4044" style="stop-color:#C41E57"/>
|
|
||||||
<stop offset="0.4677" style="stop-color:#C41E57"/>
|
|
||||||
<stop offset="0.6505" style="stop-color:#EB8523"/>
|
|
||||||
<stop offset="0.9516" style="stop-color:#FEBD11"/>
|
|
||||||
</linearGradient>
|
|
||||||
<polygon style="fill:url(#SVGID_1_);" points="49.8,15.2 36,36.7 58.4,70 70,23.1 "/>
|
|
||||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="17.7187" y1="73.2922" x2="69.5556" y2="18.1519">
|
|
||||||
<stop offset="1.612903e-002" style="stop-color:#B35BA3"/>
|
|
||||||
<stop offset="0.4044" style="stop-color:#C41E57"/>
|
|
||||||
<stop offset="0.4677" style="stop-color:#C41E57"/>
|
|
||||||
<stop offset="0.7043" style="stop-color:#EB8523"/>
|
|
||||||
</linearGradient>
|
|
||||||
<polygon style="fill:url(#SVGID_2_);" points="51.1,15.7 49,0 18.8,33.6 27.6,42.3 20.8,70 58.4,70 "/>
|
|
||||||
</g>
|
|
||||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="1.8281" y1="53.4275" x2="48.8245" y2="9.2255">
|
|
||||||
<stop offset="1.612903e-002" style="stop-color:#B35BA3"/>
|
|
||||||
<stop offset="0.6613" style="stop-color:#C41E57"/>
|
|
||||||
</linearGradient>
|
|
||||||
<polygon style="fill:url(#SVGID_3_);" points="49,0 11.6,0 0,47.1 55.6,47.1 "/>
|
|
||||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="49.8935" y1="-11.5569" x2="48.8588" y2="24.0352">
|
|
||||||
<stop offset="0.5" style="stop-color:#C41E57"/>
|
|
||||||
<stop offset="0.6668" style="stop-color:#D13F48"/>
|
|
||||||
<stop offset="0.7952" style="stop-color:#D94F39"/>
|
|
||||||
<stop offset="0.8656" style="stop-color:#DD5433"/>
|
|
||||||
</linearGradient>
|
|
||||||
<polygon style="fill:url(#SVGID_4_);" points="55.3,47.1 51.1,15.7 49,0 41.7,23 "/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
|
|
||||||
<rect x="13.4" y="13.5" transform="matrix(-1 2.577289e-003 -2.577289e-003 -1 70.0288 70.081)" style="fill:#000000;" width="43.2" height="43.2"/>
|
|
||||||
|
|
||||||
<rect x="17.6" y="48.6" transform="matrix(1 -2.577289e-003 2.577289e-003 1 -0.1287 6.634109e-002)" style="fill:#FFFFFF;" width="16.2" height="2.7"/>
|
|
||||||
<path style="fill:#FFFFFF;" d="M17.4,19.1l8.2,0c2.3,0,4,0.6,5.2,1.8c1,1,1.5,2.4,1.5,4.1l0,0.1c0,1.5-0.3,2.6-1.1,3.5
|
|
||||||
c-0.7,0.9-1.6,1.6-2.8,2l4.4,6.4l-4.6,0l-3.7-5.5l-3.3,0l0,5.5l-3.9,0L17.4,19.1z M25.3,27.8c1,0,1.7-0.2,2.2-0.7
|
|
||||||
c0.5-0.5,0.8-1.1,0.8-1.8l0-0.1c0-0.9-0.3-1.5-0.8-1.9c-0.5-0.4-1.3-0.6-2.3-0.6l-3.9,0l0,5.1L25.3,27.8z"/>
|
|
||||||
<path style="fill:#FFFFFF;" d="M36,33.2l-1.9,0l0-3.3l2.5,0l0.6-3.8l-2.3,0l0-3.3l2.8,0l0.6-3.7l3.4,0l-0.6,3.7l3.7,0l0.6-3.7
|
|
||||||
l3.4,0l-0.6,3.7l1.9,0l0,3.3l-2.5,0L47,29.9l2.3,0l0,3.3l-2.8,0L45.8,37l-3.4,0l0.7-3.8l-3.7,0L38.7,37l-3.4,0L36,33.2z
|
|
||||||
M43.7,29.9l0.6-3.8l-3.7,0L40,29.9L43.7,29.9z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.1 KiB |
@@ -1,64 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="1.7738" y1="31.2729" x2="40.1662" y2="31.2729">
|
|
||||||
<stop offset="0" style="stop-color:#905CFB"/>
|
|
||||||
<stop offset="6.772543e-002" style="stop-color:#776CF9"/>
|
|
||||||
<stop offset="0.1729" style="stop-color:#5681F7"/>
|
|
||||||
<stop offset="0.2865" style="stop-color:#3B92F5"/>
|
|
||||||
<stop offset="0.4097" style="stop-color:#269FF4"/>
|
|
||||||
<stop offset="0.5474" style="stop-color:#17A9F3"/>
|
|
||||||
<stop offset="0.7111" style="stop-color:#0FAEF2"/>
|
|
||||||
<stop offset="0.9677" style="stop-color:#0CB0F2"/>
|
|
||||||
</linearGradient>
|
|
||||||
<path style="fill:url(#SVGID_1_);" d="M39.7,47.9l-6.1-34c-0.4-2.4-1.2-4.8-2.7-7.1c-2-3.2-5.2-5.4-8.8-6.3
|
|
||||||
C7.9-2.9-2.6,11.3,3.6,23.9c0,0,0,0,0,0l14.8,31.7c0.4,1,1,2,1.7,2.9c1.2,1.6,2.8,2.8,4.7,3.4C34.4,64.9,42.1,56.4,39.7,47.9z"/>
|
|
||||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="5.3113" y1="9.6691" x2="69.2278" y2="43.8664">
|
|
||||||
<stop offset="0" style="stop-color:#905CFB"/>
|
|
||||||
<stop offset="6.772543e-002" style="stop-color:#776CF9"/>
|
|
||||||
<stop offset="0.1729" style="stop-color:#5681F7"/>
|
|
||||||
<stop offset="0.2865" style="stop-color:#3B92F5"/>
|
|
||||||
<stop offset="0.4097" style="stop-color:#269FF4"/>
|
|
||||||
<stop offset="0.5474" style="stop-color:#17A9F3"/>
|
|
||||||
<stop offset="0.7111" style="stop-color:#0FAEF2"/>
|
|
||||||
<stop offset="0.9677" style="stop-color:#0CB0F2"/>
|
|
||||||
</linearGradient>
|
|
||||||
<path style="fill:url(#SVGID_2_);" d="M67.4,26.5c-1.4-2.2-3.4-3.9-5.7-4.9L25.5,1.7l0,0c-1-0.5-2.1-1-3.3-1.3
|
|
||||||
C6.7-3.2-4.4,13.8,5.5,27c1.5,2,3.6,3.6,6,4.5L48,47.9c0.8,0.5,1.6,0.8,2.5,1.1C64.5,53.4,75.1,38.6,67.4,26.5z"/>
|
|
||||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="-19.2836" y1="70.8198" x2="55.9833" y2="33.1863">
|
|
||||||
<stop offset="0" style="stop-color:#3BEA62"/>
|
|
||||||
<stop offset="0.117" style="stop-color:#31DE80"/>
|
|
||||||
<stop offset="0.3025" style="stop-color:#24CEA8"/>
|
|
||||||
<stop offset="0.4844" style="stop-color:#1AC1C9"/>
|
|
||||||
<stop offset="0.6592" style="stop-color:#12B7DF"/>
|
|
||||||
<stop offset="0.8238" style="stop-color:#0EB2ED"/>
|
|
||||||
<stop offset="0.9677" style="stop-color:#0CB0F2"/>
|
|
||||||
</linearGradient>
|
|
||||||
<path style="fill:url(#SVGID_3_);" d="M67.4,26.5c-1.8-2.8-4.6-4.8-7.9-5.6c-3.5-0.8-6.8-0.5-9.6,0.7L11.4,36.1
|
|
||||||
c0,0-0.2,0.1-0.6,0.4C0.9,40.4-4,53.3,4,64c1.8,2.4,4.3,4.2,7.1,5c5.3,1.6,10.1,1,14-1.1c0,0,0.1,0,0.1,0l37.6-20.1
|
|
||||||
c0,0,0,0,0.1-0.1C69.5,43.9,72.6,34.6,67.4,26.5z"/>
|
|
||||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="38.9439" y1="5.8503" x2="5.4232" y2="77.5093">
|
|
||||||
<stop offset="0" style="stop-color:#3BEA62"/>
|
|
||||||
<stop offset="9.397750e-002" style="stop-color:#2FDB87"/>
|
|
||||||
<stop offset="0.196" style="stop-color:#24CEA8"/>
|
|
||||||
<stop offset="0.3063" style="stop-color:#1BC3C3"/>
|
|
||||||
<stop offset="0.4259" style="stop-color:#14BAD8"/>
|
|
||||||
<stop offset="0.5596" style="stop-color:#10B5E7"/>
|
|
||||||
<stop offset="0.7185" style="stop-color:#0DB1EF"/>
|
|
||||||
<stop offset="0.9677" style="stop-color:#0CB0F2"/>
|
|
||||||
</linearGradient>
|
|
||||||
<path style="fill:url(#SVGID_4_);" d="M50.3,12.8c1.2-2.7,1.1-6-0.9-9c-1.1-1.8-2.9-3-4.9-3.5c-4.5-1.1-8.3,1-10.1,4.2L3.5,42
|
|
||||||
c0,0,0,0,0,0.1C-0.9,47.9-1.6,56.5,4,64c1.8,2.4,4.3,4.2,7.1,5c10.5,3.3,19.3-2.5,22.1-10.8L50.3,12.8z"/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<rect x="13.4" y="13.4" style="fill:#000000;" width="43.2" height="43.2"/>
|
|
||||||
<rect x="17.5" y="48.5" style="fill:#FFFFFF;" width="16.2" height="2.7"/>
|
|
||||||
<polygon style="fill:#FFFFFF;" points="22.9,22.7 17.5,22.7 17.5,19.1 32.3,19.1 32.3,22.7 26.8,22.7 26.8,37 22.9,37 "/>
|
|
||||||
<path style="fill:#FFFFFF;" d="M32.5,28.1L32.5,28.1c0-5.1,3.8-9.3,9.3-9.3c3.4,0,5.4,1.1,7.1,2.8l-2.5,2.9c-1.4-1.3-2.8-2-4.6-2
|
|
||||||
c-3,0-5.2,2.5-5.2,5.6V28c0,3.1,2.1,5.6,5.2,5.6c2,0,3.3-0.8,4.7-2.1l2.5,2.5c-1.8,2-3.9,3.2-7.3,3.2
|
|
||||||
C36.4,37.3,32.5,33.2,32.5,28.1"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.0 KiB |
@@ -1,4 +1,4 @@
|
|||||||
# <img width="24px" src="./Logo/256.png" alt="Sonarr"></img> Sonarr
|
# <img width="24px" src="./Logo/256.png" alt="Sonarr"></img> Sonarr
|
||||||
|
|
||||||
[](https://translate.servarr.com/engage/servarr/)
|
[](https://translate.servarr.com/engage/servarr/)
|
||||||
[](#backers)
|
[](#backers)
|
||||||
@@ -33,7 +33,7 @@ Note: GitHub Issues are for Bugs and Feature Requests Only
|
|||||||
- Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
|
- Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
|
||||||
- Automatically detects new episodes
|
- Automatically detects new episodes
|
||||||
- Can scan your existing library and download any missing episodes
|
- Can scan your existing library and download any missing episodes
|
||||||
- Can watch for better quality of the episodes you already have and do an automatic upgrade. *eg. from DVD to Blu-Ray*
|
- Can watch for better quality of the episodes you already have and do an automatic upgrade. _eg. from DVD to Blu-Ray_
|
||||||
- Automatic failed download handling will try another release if one fails
|
- Automatic failed download handling will try another release if one fails
|
||||||
- Manual search so you can pick any release or to see why a release was not downloaded automatically
|
- Manual search so you can pick any release or to see why a release was not downloaded automatically
|
||||||
- Fully configurable episode renaming
|
- Fully configurable episode renaming
|
||||||
@@ -52,7 +52,7 @@ This project exists thanks to all the people who contribute. [Contribute](CONTRI
|
|||||||
|
|
||||||
### Supporters
|
### Supporters
|
||||||
|
|
||||||
This project would not be possible without the support of our users and software providers.
|
This project would not be possible without the support of our users and software providers.
|
||||||
[**Become a sponsor or backer**](https://opencollective.com/sonarr) to help us out!
|
[**Become a sponsor or backer**](https://opencollective.com/sonarr) to help us out!
|
||||||
|
|
||||||
#### Mega Sponsors
|
#### Mega Sponsors
|
||||||
@@ -69,13 +69,17 @@ This project would not be possible without the support of our users and software
|
|||||||
|
|
||||||
#### JetBrains
|
#### JetBrains
|
||||||
|
|
||||||
Thank you to [<img src="/Logo/Jetbrains/jetbrains.svg" alt="JetBrains" width="32"> JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools
|
Thank you to [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.png" alt="JetBrains" width="96">](http://www.jetbrains.com/) for providing us with free licenses to their great tools
|
||||||
|
|
||||||
* [<img src="/Logo/Jetbrains/teamcity.svg" alt="TeamCity" width="32"> TeamCity](http://www.jetbrains.com/teamcity/)
|
[<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/TeamCity.png" alt="TeamCity" width="64">](http://www.jetbrains.com/teamcity/)
|
||||||
* [<img src="/Logo/Jetbrains/resharper.svg" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
|
|
||||||
* [<img src="/Logo/Jetbrains/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
|
[<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/ReSharper.png" alt="ReSharper" width="64">](http://www.jetbrains.com/resharper/)
|
||||||
|
|
||||||
|
[<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/dotTrace.png" alt="dotTrace" width="64">](http://www.jetbrains.com/dottrace/)
|
||||||
|
|
||||||
|
[<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/Rider.png" alt="Rider" width="64">](http://www.jetbrains.com/rider/)
|
||||||
|
|
||||||
### Licenses
|
### Licenses
|
||||||
|
|
||||||
- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||||
- Copyright 2010-2023
|
- Copyright 2010-2024
|
||||||
|
|||||||
@@ -210,7 +210,6 @@ module.exports = {
|
|||||||
'no-undef-init': 'off',
|
'no-undef-init': 'off',
|
||||||
'no-undefined': 'off',
|
'no-undefined': 'off',
|
||||||
'no-unused-vars': ['error', { args: 'none', ignoreRestSiblings: true }],
|
'no-unused-vars': ['error', { args: 'none', ignoreRestSiblings: true }],
|
||||||
'no-use-before-define': 'error',
|
|
||||||
|
|
||||||
// Node.js and CommonJS
|
// Node.js and CommonJS
|
||||||
|
|
||||||
|
|||||||
@@ -195,9 +195,14 @@ function HistoryRow(props: HistoryRowProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'downloadClient') {
|
if (name === 'downloadClient') {
|
||||||
|
const downloadClientName =
|
||||||
|
'downloadClientName' in data ? data.downloadClientName : null;
|
||||||
|
const downloadClient =
|
||||||
|
'downloadClient' in data ? data.downloadClient : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRowCell key={name} className={styles.downloadClient}>
|
<TableRowCell key={name} className={styles.downloadClient}>
|
||||||
{'downloadClient' in data ? data.downloadClient : ''}
|
{downloadClientName ?? downloadClient ?? ''}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
import TextInput from 'Components/Form/TextInput';
|
import TextInput from 'Components/Form/TextInput';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
@@ -129,7 +130,8 @@ class AddNewSeries extends Component {
|
|||||||
<div className={styles.helpText}>
|
<div className={styles.helpText}>
|
||||||
{translate('AddNewSeriesError')}
|
{translate('AddNewSeriesError')}
|
||||||
</div>
|
</div>
|
||||||
<div>{getErrorMessage(error)}</div>
|
|
||||||
|
<Alert kind={kinds.DANGER}>{getErrorMessage(error)}</Alert>
|
||||||
</div> : null
|
</div> : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import BlocklistAppState from './BlocklistAppState';
|
import BlocklistAppState from './BlocklistAppState';
|
||||||
import CalendarAppState from './CalendarAppState';
|
import CalendarAppState from './CalendarAppState';
|
||||||
|
import CaptchaAppState from './CaptchaAppState';
|
||||||
import CommandAppState from './CommandAppState';
|
import CommandAppState from './CommandAppState';
|
||||||
import EpisodeFilesAppState from './EpisodeFilesAppState';
|
import EpisodeFilesAppState from './EpisodeFilesAppState';
|
||||||
import EpisodesAppState from './EpisodesAppState';
|
import EpisodesAppState from './EpisodesAppState';
|
||||||
import HistoryAppState from './HistoryAppState';
|
import HistoryAppState from './HistoryAppState';
|
||||||
import InteractiveImportAppState from './InteractiveImportAppState';
|
import InteractiveImportAppState from './InteractiveImportAppState';
|
||||||
|
import OAuthAppState from './OAuthAppState';
|
||||||
import ParseAppState from './ParseAppState';
|
import ParseAppState from './ParseAppState';
|
||||||
import PathsAppState from './PathsAppState';
|
import PathsAppState from './PathsAppState';
|
||||||
|
import ProviderOptionsAppState from './ProviderOptionsAppState';
|
||||||
import QueueAppState from './QueueAppState';
|
import QueueAppState from './QueueAppState';
|
||||||
import ReleasesAppState from './ReleasesAppState';
|
import ReleasesAppState from './ReleasesAppState';
|
||||||
import RootFolderAppState from './RootFolderAppState';
|
import RootFolderAppState from './RootFolderAppState';
|
||||||
@@ -64,14 +67,17 @@ interface AppState {
|
|||||||
app: AppSectionState;
|
app: AppSectionState;
|
||||||
blocklist: BlocklistAppState;
|
blocklist: BlocklistAppState;
|
||||||
calendar: CalendarAppState;
|
calendar: CalendarAppState;
|
||||||
|
captcha: CaptchaAppState;
|
||||||
commands: CommandAppState;
|
commands: CommandAppState;
|
||||||
episodeFiles: EpisodeFilesAppState;
|
episodeFiles: EpisodeFilesAppState;
|
||||||
episodes: EpisodesAppState;
|
episodes: EpisodesAppState;
|
||||||
episodesSelection: EpisodesAppState;
|
episodesSelection: EpisodesAppState;
|
||||||
history: HistoryAppState;
|
history: HistoryAppState;
|
||||||
interactiveImport: InteractiveImportAppState;
|
interactiveImport: InteractiveImportAppState;
|
||||||
|
oAuth: OAuthAppState;
|
||||||
parse: ParseAppState;
|
parse: ParseAppState;
|
||||||
paths: PathsAppState;
|
paths: PathsAppState;
|
||||||
|
providerOptions: ProviderOptionsAppState;
|
||||||
queue: QueueAppState;
|
queue: QueueAppState;
|
||||||
releases: ReleasesAppState;
|
releases: ReleasesAppState;
|
||||||
rootFolders: RootFolderAppState;
|
rootFolders: RootFolderAppState;
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
interface CaptchaAppState {
|
||||||
|
refreshing: false;
|
||||||
|
token: string;
|
||||||
|
siteKey: unknown;
|
||||||
|
secretToken: unknown;
|
||||||
|
ray: unknown;
|
||||||
|
stoken: unknown;
|
||||||
|
responseUrl: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CaptchaAppState;
|
||||||
@@ -1,11 +1,20 @@
|
|||||||
import AppSectionState from 'App/State/AppSectionState';
|
import AppSectionState from 'App/State/AppSectionState';
|
||||||
import RecentFolder from 'InteractiveImport/Folder/RecentFolder';
|
|
||||||
import ImportMode from 'InteractiveImport/ImportMode';
|
import ImportMode from 'InteractiveImport/ImportMode';
|
||||||
import InteractiveImport from 'InteractiveImport/InteractiveImport';
|
import InteractiveImport from 'InteractiveImport/InteractiveImport';
|
||||||
|
|
||||||
|
interface FavoriteFolder {
|
||||||
|
folder: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RecentFolder {
|
||||||
|
folder: string;
|
||||||
|
lastUsed: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface InteractiveImportAppState extends AppSectionState<InteractiveImport> {
|
interface InteractiveImportAppState extends AppSectionState<InteractiveImport> {
|
||||||
originalItems: InteractiveImport[];
|
originalItems: InteractiveImport[];
|
||||||
importMode: ImportMode;
|
importMode: ImportMode;
|
||||||
|
favoriteFolders: FavoriteFolder[];
|
||||||
recentFolders: RecentFolder[];
|
recentFolders: RecentFolder[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Error } from './AppSectionState';
|
||||||
|
|
||||||
|
interface OAuthAppState {
|
||||||
|
authorizing: boolean;
|
||||||
|
result: Record<string, unknown> | null;
|
||||||
|
error: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OAuthAppState;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import AppSectionState from 'App/State/AppSectionState';
|
||||||
|
import Field, { FieldSelectOption } from 'typings/Field';
|
||||||
|
|
||||||
|
export interface ProviderOptions {
|
||||||
|
fields?: Field[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProviderOptionsDevice {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProviderOptionsAppState {
|
||||||
|
devices: AppSectionState<ProviderOptionsDevice>;
|
||||||
|
servers: AppSectionState<FieldSelectOption<unknown>>;
|
||||||
|
newznabCategories: AppSectionState<FieldSelectOption<unknown>>;
|
||||||
|
getProfiles: AppSectionState<FieldSelectOption<unknown>>;
|
||||||
|
getTags: AppSectionState<FieldSelectOption<unknown>>;
|
||||||
|
getRootFolders: AppSectionState<FieldSelectOption<unknown>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProviderOptionsAppState;
|
||||||
@@ -16,6 +16,9 @@ import IndexerFlag from 'typings/IndexerFlag';
|
|||||||
import Notification from 'typings/Notification';
|
import Notification from 'typings/Notification';
|
||||||
import QualityProfile from 'typings/QualityProfile';
|
import QualityProfile from 'typings/QualityProfile';
|
||||||
import General from 'typings/Settings/General';
|
import General from 'typings/Settings/General';
|
||||||
|
import NamingConfig from 'typings/Settings/NamingConfig';
|
||||||
|
import NamingExample from 'typings/Settings/NamingExample';
|
||||||
|
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
|
||||||
import UiSettings from 'typings/Settings/UiSettings';
|
import UiSettings from 'typings/Settings/UiSettings';
|
||||||
|
|
||||||
export interface DownloadClientAppState
|
export interface DownloadClientAppState
|
||||||
@@ -29,6 +32,13 @@ export interface GeneralAppState
|
|||||||
extends AppSectionItemState<General>,
|
extends AppSectionItemState<General>,
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
export interface NamingAppState
|
||||||
|
extends AppSectionItemState<NamingConfig>,
|
||||||
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
export interface NamingExamplesAppState
|
||||||
|
extends AppSectionItemState<NamingExample> {}
|
||||||
|
|
||||||
export interface ImportListAppState
|
export interface ImportListAppState
|
||||||
extends AppSectionState<ImportList>,
|
extends AppSectionState<ImportList>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
@@ -49,6 +59,12 @@ export interface QualityProfilesAppState
|
|||||||
extends AppSectionState<QualityProfile>,
|
extends AppSectionState<QualityProfile>,
|
||||||
AppSectionItemSchemaState<QualityProfile> {}
|
AppSectionItemSchemaState<QualityProfile> {}
|
||||||
|
|
||||||
|
export interface ReleaseProfilesAppState
|
||||||
|
extends AppSectionState<ReleaseProfile>,
|
||||||
|
AppSectionSaveState {
|
||||||
|
pendingChanges: Partial<ReleaseProfile>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CustomFormatAppState
|
export interface CustomFormatAppState
|
||||||
extends AppSectionState<CustomFormat>,
|
extends AppSectionState<CustomFormat>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
@@ -81,8 +97,11 @@ interface SettingsAppState {
|
|||||||
indexerFlags: IndexerFlagSettingsAppState;
|
indexerFlags: IndexerFlagSettingsAppState;
|
||||||
indexers: IndexerAppState;
|
indexers: IndexerAppState;
|
||||||
languages: LanguageSettingsAppState;
|
languages: LanguageSettingsAppState;
|
||||||
|
naming: NamingAppState;
|
||||||
|
namingExamples: NamingExamplesAppState;
|
||||||
notifications: NotificationAppState;
|
notifications: NotificationAppState;
|
||||||
qualityProfiles: QualityProfilesAppState;
|
qualityProfiles: QualityProfilesAppState;
|
||||||
|
releaseProfiles: ReleaseProfilesAppState;
|
||||||
ui: UiSettingsAppState;
|
ui: UiSettingsAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import PathInput from 'Components/Form/PathInput';
|
import { PathInputInternal } from 'Components/Form/PathInput';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||||
@@ -151,7 +151,7 @@ function FileBrowserModalContent(props: FileBrowserModalContentProps) {
|
|||||||
</Alert>
|
</Alert>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<PathInput
|
<PathInputInternal
|
||||||
className={styles.pathInput}
|
className={styles.pathInput}
|
||||||
placeholder={translate('FileBrowserPlaceholderText')}
|
placeholder={translate('FileBrowserPlaceholderText')}
|
||||||
hasFileBrowser={false}
|
hasFileBrowser={false}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import LanguageFilterBuilderRowValue from './LanguageFilterBuilderRowValue';
|
|||||||
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
||||||
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
||||||
import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue';
|
import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue';
|
||||||
|
import QueueStatusFilterBuilderRowValue from './QueueStatusFilterBuilderRowValue';
|
||||||
import SeasonsMonitoredStatusFilterBuilderRowValue from './SeasonsMonitoredStatusFilterBuilderRowValue';
|
import SeasonsMonitoredStatusFilterBuilderRowValue from './SeasonsMonitoredStatusFilterBuilderRowValue';
|
||||||
import SeriesFilterBuilderRowValue from './SeriesFilterBuilderRowValue';
|
import SeriesFilterBuilderRowValue from './SeriesFilterBuilderRowValue';
|
||||||
import SeriesStatusFilterBuilderRowValue from './SeriesStatusFilterBuilderRowValue';
|
import SeriesStatusFilterBuilderRowValue from './SeriesStatusFilterBuilderRowValue';
|
||||||
@@ -80,6 +81,9 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
|||||||
case filterBuilderValueTypes.QUALITY_PROFILE:
|
case filterBuilderValueTypes.QUALITY_PROFILE:
|
||||||
return QualityProfileFilterBuilderRowValue;
|
return QualityProfileFilterBuilderRowValue;
|
||||||
|
|
||||||
|
case filterBuilderValueTypes.QUEUE_STATUS:
|
||||||
|
return QueueStatusFilterBuilderRowValue;
|
||||||
|
|
||||||
case filterBuilderValueTypes.SEASONS_MONITORED_STATUS:
|
case filterBuilderValueTypes.SEASONS_MONITORED_STATUS:
|
||||||
return SeasonsMonitoredStatusFilterBuilderRowValue;
|
return SeasonsMonitoredStatusFilterBuilderRowValue;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import TagInput from 'Components/Form/TagInput';
|
import TagInput from 'Components/Form/Tag/TagInput';
|
||||||
import { filterBuilderTypes, filterBuilderValueTypes, kinds } from 'Helpers/Props';
|
import { filterBuilderTypes, filterBuilderValueTypes, kinds } from 'Helpers/Props';
|
||||||
import tagShape from 'Helpers/Props/Shapes/tagShape';
|
import tagShape from 'Helpers/Props/Shapes/tagShape';
|
||||||
import convertToBytes from 'Utilities/Number/convertToBytes';
|
import convertToBytes from 'Utilities/Number/convertToBytes';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import TagInputTag from 'Components/Form/TagInputTag';
|
import TagInputTag from 'Components/Form/Tag/TagInputTag';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './FilterBuilderRowValueTag.css';
|
import styles from './FilterBuilderRowValueTag.css';
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||||
|
|
||||||
|
const statusTagList = [
|
||||||
|
{
|
||||||
|
id: 'queued',
|
||||||
|
get name() {
|
||||||
|
return translate('Queued');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'paused',
|
||||||
|
get name() {
|
||||||
|
return translate('Paused');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'downloading',
|
||||||
|
get name() {
|
||||||
|
return translate('Downloading');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'completed',
|
||||||
|
get name() {
|
||||||
|
return translate('Completed');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'failed',
|
||||||
|
get name() {
|
||||||
|
return translate('Failed');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'warning',
|
||||||
|
get name() {
|
||||||
|
return translate('Warning');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'delay',
|
||||||
|
get name() {
|
||||||
|
return translate('Delay');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'downloadClientUnavailable',
|
||||||
|
get name() {
|
||||||
|
return translate('DownloadClientUnavailable');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fallback',
|
||||||
|
get name() {
|
||||||
|
return translate('Fallback');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function QueueStatusFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
|
||||||
|
return <FilterBuilderRowValue {...props} tagList={statusTagList} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QueueStatusFilterBuilderRowValue;
|
||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
|
||||||
const seriesStatusList = [
|
const statusTagList = [
|
||||||
{
|
{
|
||||||
id: 'continuing',
|
id: 'continuing',
|
||||||
get name() {
|
get name() {
|
||||||
@@ -32,7 +32,7 @@ const seriesStatusList = [
|
|||||||
function SeriesStatusFilterBuilderRowValue(props) {
|
function SeriesStatusFilterBuilderRowValue(props) {
|
||||||
return (
|
return (
|
||||||
<FilterBuilderRowValue
|
<FilterBuilderRowValue
|
||||||
tagList={seriesStatusList}
|
tagList={statusTagList}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
import jdu from 'jdu';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import AutoSuggestInput from './AutoSuggestInput';
|
|
||||||
|
|
||||||
class AutoCompleteInput extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
suggestions: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
getSuggestionValue(item) {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSuggestion(item) {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onInputChange = (event, { newValue }) => {
|
|
||||||
this.props.onChange({
|
|
||||||
name: this.props.name,
|
|
||||||
value: newValue
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onInputBlur = () => {
|
|
||||||
this.setState({ suggestions: [] });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSuggestionsFetchRequested = ({ value }) => {
|
|
||||||
const { values } = this.props;
|
|
||||||
const lowerCaseValue = jdu.replace(value).toLowerCase();
|
|
||||||
|
|
||||||
const filteredValues = values.filter((v) => {
|
|
||||||
return jdu.replace(v).toLowerCase().contains(lowerCaseValue);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState({ suggestions: filteredValues });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSuggestionsClearRequested = () => {
|
|
||||||
this.setState({ suggestions: [] });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const { suggestions } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AutoSuggestInput
|
|
||||||
{...otherProps}
|
|
||||||
name={name}
|
|
||||||
value={value}
|
|
||||||
suggestions={suggestions}
|
|
||||||
getSuggestionValue={this.getSuggestionValue}
|
|
||||||
renderSuggestion={this.renderSuggestion}
|
|
||||||
onInputBlur={this.onInputBlur}
|
|
||||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
|
||||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AutoCompleteInput.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.string,
|
|
||||||
values: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
AutoCompleteInput.defaultProps = {
|
|
||||||
value: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AutoCompleteInput;
|
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import jdu from 'jdu';
|
||||||
|
import React, { SyntheticEvent, useCallback, useState } from 'react';
|
||||||
|
import {
|
||||||
|
ChangeEvent,
|
||||||
|
SuggestionsFetchRequestedParams,
|
||||||
|
} from 'react-autosuggest';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import AutoSuggestInput from './AutoSuggestInput';
|
||||||
|
|
||||||
|
interface AutoCompleteInputProps {
|
||||||
|
name: string;
|
||||||
|
value?: string;
|
||||||
|
values: string[];
|
||||||
|
onChange: (change: InputChanged<string>) => unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AutoCompleteInput({
|
||||||
|
name,
|
||||||
|
value = '',
|
||||||
|
values,
|
||||||
|
onChange,
|
||||||
|
...otherProps
|
||||||
|
}: AutoCompleteInputProps) {
|
||||||
|
const [suggestions, setSuggestions] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const getSuggestionValue = useCallback((item: string) => {
|
||||||
|
return item;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderSuggestion = useCallback((item: string) => {
|
||||||
|
return item;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleInputChange = useCallback(
|
||||||
|
(_event: SyntheticEvent, { newValue }: ChangeEvent) => {
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: newValue,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[name, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleInputBlur = useCallback(() => {
|
||||||
|
setSuggestions([]);
|
||||||
|
}, [setSuggestions]);
|
||||||
|
|
||||||
|
const handleSuggestionsFetchRequested = useCallback(
|
||||||
|
({ value: newValue }: SuggestionsFetchRequestedParams) => {
|
||||||
|
const lowerCaseValue = jdu.replace(newValue).toLowerCase();
|
||||||
|
|
||||||
|
const filteredValues = values.filter((v) => {
|
||||||
|
return jdu.replace(v).toLowerCase().includes(lowerCaseValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
setSuggestions(filteredValues);
|
||||||
|
},
|
||||||
|
[values, setSuggestions]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSuggestionsClearRequested = useCallback(() => {
|
||||||
|
setSuggestions([]);
|
||||||
|
}, [setSuggestions]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AutoSuggestInput
|
||||||
|
{...otherProps}
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
suggestions={suggestions}
|
||||||
|
getSuggestionValue={getSuggestionValue}
|
||||||
|
renderSuggestion={renderSuggestion}
|
||||||
|
onInputChange={handleInputChange}
|
||||||
|
onInputBlur={handleInputBlur}
|
||||||
|
onSuggestionsFetchRequested={handleSuggestionsFetchRequested}
|
||||||
|
onSuggestionsClearRequested={handleSuggestionsClearRequested}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AutoCompleteInput;
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Autosuggest from 'react-autosuggest';
|
|
||||||
import { Manager, Popper, Reference } from 'react-popper';
|
|
||||||
import Portal from 'Components/Portal';
|
|
||||||
import styles from './AutoSuggestInput.css';
|
|
||||||
|
|
||||||
class AutoSuggestInput extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._scheduleUpdate = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (
|
|
||||||
this._scheduleUpdate &&
|
|
||||||
prevProps.suggestions !== this.props.suggestions
|
|
||||||
) {
|
|
||||||
this._scheduleUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
renderInputComponent = (inputProps) => {
|
|
||||||
const { renderInputComponent } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Reference>
|
|
||||||
{({ ref }) => {
|
|
||||||
if (renderInputComponent) {
|
|
||||||
return renderInputComponent(inputProps, ref);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={ref}>
|
|
||||||
<input
|
|
||||||
{...inputProps}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Reference>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
renderSuggestionsContainer = ({ containerProps, children }) => {
|
|
||||||
return (
|
|
||||||
<Portal>
|
|
||||||
<Popper
|
|
||||||
placement='bottom-start'
|
|
||||||
modifiers={{
|
|
||||||
computeMaxHeight: {
|
|
||||||
order: 851,
|
|
||||||
enabled: true,
|
|
||||||
fn: this.onComputeMaxHeight
|
|
||||||
},
|
|
||||||
flip: {
|
|
||||||
padding: this.props.minHeight
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{({ ref: popperRef, style, scheduleUpdate }) => {
|
|
||||||
this._scheduleUpdate = scheduleUpdate;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={popperRef}
|
|
||||||
style={style}
|
|
||||||
className={children ? styles.suggestionsContainerOpen : undefined}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
{...containerProps}
|
|
||||||
style={{
|
|
||||||
maxHeight: style.maxHeight
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Popper>
|
|
||||||
</Portal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onComputeMaxHeight = (data) => {
|
|
||||||
const {
|
|
||||||
top,
|
|
||||||
bottom,
|
|
||||||
width
|
|
||||||
} = data.offsets.reference;
|
|
||||||
|
|
||||||
const windowHeight = window.innerHeight;
|
|
||||||
|
|
||||||
if ((/^botton/).test(data.placement)) {
|
|
||||||
data.styles.maxHeight = windowHeight - bottom;
|
|
||||||
} else {
|
|
||||||
data.styles.maxHeight = top;
|
|
||||||
}
|
|
||||||
|
|
||||||
data.styles.width = width;
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
onInputChange = (event, { newValue }) => {
|
|
||||||
this.props.onChange({
|
|
||||||
name: this.props.name,
|
|
||||||
value: newValue
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onInputKeyDown = (event) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
suggestions,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (
|
|
||||||
event.key === 'Tab' &&
|
|
||||||
suggestions.length &&
|
|
||||||
suggestions[0] !== this.props.value
|
|
||||||
) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
if (value) {
|
|
||||||
onChange({
|
|
||||||
name,
|
|
||||||
value: suggestions[0]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
forwardedRef,
|
|
||||||
className,
|
|
||||||
inputContainerClassName,
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
placeholder,
|
|
||||||
suggestions,
|
|
||||||
hasError,
|
|
||||||
hasWarning,
|
|
||||||
getSuggestionValue,
|
|
||||||
renderSuggestion,
|
|
||||||
onInputChange,
|
|
||||||
onInputKeyDown,
|
|
||||||
onInputFocus,
|
|
||||||
onInputBlur,
|
|
||||||
onSuggestionsFetchRequested,
|
|
||||||
onSuggestionsClearRequested,
|
|
||||||
onSuggestionSelected,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const inputProps = {
|
|
||||||
className: classNames(
|
|
||||||
className,
|
|
||||||
hasError && styles.hasError,
|
|
||||||
hasWarning && styles.hasWarning
|
|
||||||
),
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
placeholder,
|
|
||||||
autoComplete: 'off',
|
|
||||||
spellCheck: false,
|
|
||||||
onChange: onInputChange || this.onInputChange,
|
|
||||||
onKeyDown: onInputKeyDown || this.onInputKeyDown,
|
|
||||||
onFocus: onInputFocus,
|
|
||||||
onBlur: onInputBlur
|
|
||||||
};
|
|
||||||
|
|
||||||
const theme = {
|
|
||||||
container: inputContainerClassName,
|
|
||||||
containerOpen: styles.suggestionsContainerOpen,
|
|
||||||
suggestionsContainer: styles.suggestionsContainer,
|
|
||||||
suggestionsList: styles.suggestionsList,
|
|
||||||
suggestion: styles.suggestion,
|
|
||||||
suggestionHighlighted: styles.suggestionHighlighted
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Manager>
|
|
||||||
<Autosuggest
|
|
||||||
{...otherProps}
|
|
||||||
ref={forwardedRef}
|
|
||||||
id={name}
|
|
||||||
inputProps={inputProps}
|
|
||||||
theme={theme}
|
|
||||||
suggestions={suggestions}
|
|
||||||
getSuggestionValue={getSuggestionValue}
|
|
||||||
renderInputComponent={this.renderInputComponent}
|
|
||||||
renderSuggestionsContainer={this.renderSuggestionsContainer}
|
|
||||||
renderSuggestion={renderSuggestion}
|
|
||||||
onSuggestionSelected={onSuggestionSelected}
|
|
||||||
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
|
|
||||||
onSuggestionsClearRequested={onSuggestionsClearRequested}
|
|
||||||
/>
|
|
||||||
</Manager>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AutoSuggestInput.propTypes = {
|
|
||||||
forwardedRef: PropTypes.func,
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
inputContainerClassName: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
|
|
||||||
placeholder: PropTypes.string,
|
|
||||||
suggestions: PropTypes.array.isRequired,
|
|
||||||
hasError: PropTypes.bool,
|
|
||||||
hasWarning: PropTypes.bool,
|
|
||||||
enforceMaxHeight: PropTypes.bool.isRequired,
|
|
||||||
minHeight: PropTypes.number.isRequired,
|
|
||||||
maxHeight: PropTypes.number.isRequired,
|
|
||||||
getSuggestionValue: PropTypes.func.isRequired,
|
|
||||||
renderInputComponent: PropTypes.elementType,
|
|
||||||
renderSuggestion: PropTypes.func.isRequired,
|
|
||||||
onInputChange: PropTypes.func,
|
|
||||||
onInputKeyDown: PropTypes.func,
|
|
||||||
onInputFocus: PropTypes.func,
|
|
||||||
onInputBlur: PropTypes.func.isRequired,
|
|
||||||
onSuggestionsFetchRequested: PropTypes.func.isRequired,
|
|
||||||
onSuggestionsClearRequested: PropTypes.func.isRequired,
|
|
||||||
onSuggestionSelected: PropTypes.func,
|
|
||||||
onChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
AutoSuggestInput.defaultProps = {
|
|
||||||
className: styles.input,
|
|
||||||
inputContainerClassName: styles.inputContainer,
|
|
||||||
enforceMaxHeight: true,
|
|
||||||
minHeight: 50,
|
|
||||||
maxHeight: 200
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AutoSuggestInput;
|
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, {
|
||||||
|
FocusEvent,
|
||||||
|
FormEvent,
|
||||||
|
KeyboardEvent,
|
||||||
|
KeyboardEventHandler,
|
||||||
|
MutableRefObject,
|
||||||
|
ReactNode,
|
||||||
|
Ref,
|
||||||
|
SyntheticEvent,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
|
import Autosuggest, {
|
||||||
|
AutosuggestPropsBase,
|
||||||
|
BlurEvent,
|
||||||
|
ChangeEvent,
|
||||||
|
RenderInputComponentProps,
|
||||||
|
RenderSuggestionsContainerParams,
|
||||||
|
} from 'react-autosuggest';
|
||||||
|
import { Manager, Popper, Reference } from 'react-popper';
|
||||||
|
import Portal from 'Components/Portal';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import styles from './AutoSuggestInput.css';
|
||||||
|
|
||||||
|
interface AutoSuggestInputProps<T>
|
||||||
|
extends Omit<AutosuggestPropsBase<T>, 'renderInputComponent' | 'inputProps'> {
|
||||||
|
forwardedRef?: MutableRefObject<Autosuggest<T> | null>;
|
||||||
|
className?: string;
|
||||||
|
inputContainerClassName?: string;
|
||||||
|
name: string;
|
||||||
|
value?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
suggestions: T[];
|
||||||
|
hasError?: boolean;
|
||||||
|
hasWarning?: boolean;
|
||||||
|
enforceMaxHeight?: boolean;
|
||||||
|
minHeight?: number;
|
||||||
|
maxHeight?: number;
|
||||||
|
renderInputComponent?: (
|
||||||
|
inputProps: RenderInputComponentProps,
|
||||||
|
ref: Ref<HTMLDivElement>
|
||||||
|
) => ReactNode;
|
||||||
|
onInputChange: (
|
||||||
|
event: FormEvent<HTMLElement>,
|
||||||
|
params: ChangeEvent
|
||||||
|
) => unknown;
|
||||||
|
onInputKeyDown?: KeyboardEventHandler<HTMLElement>;
|
||||||
|
onInputFocus?: (event: SyntheticEvent) => unknown;
|
||||||
|
onInputBlur: (
|
||||||
|
event: FocusEvent<HTMLElement>,
|
||||||
|
params?: BlurEvent<T>
|
||||||
|
) => unknown;
|
||||||
|
onChange?: (change: InputChanged<T>) => unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
function AutoSuggestInput<T = any>(props: AutoSuggestInputProps<T>) {
|
||||||
|
const {
|
||||||
|
// TODO: forwaredRef should be replaces with React.forwardRef
|
||||||
|
forwardedRef,
|
||||||
|
className = styles.input,
|
||||||
|
inputContainerClassName = styles.inputContainer,
|
||||||
|
name,
|
||||||
|
value = '',
|
||||||
|
placeholder,
|
||||||
|
suggestions,
|
||||||
|
enforceMaxHeight = true,
|
||||||
|
hasError,
|
||||||
|
hasWarning,
|
||||||
|
minHeight = 50,
|
||||||
|
maxHeight = 200,
|
||||||
|
getSuggestionValue,
|
||||||
|
renderSuggestion,
|
||||||
|
renderInputComponent,
|
||||||
|
onInputChange,
|
||||||
|
onInputKeyDown,
|
||||||
|
onInputFocus,
|
||||||
|
onInputBlur,
|
||||||
|
onSuggestionsFetchRequested,
|
||||||
|
onSuggestionsClearRequested,
|
||||||
|
onSuggestionSelected,
|
||||||
|
onChange,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const updater = useRef<(() => void) | null>(null);
|
||||||
|
const previousSuggestions = usePrevious(suggestions);
|
||||||
|
|
||||||
|
const handleComputeMaxHeight = useCallback(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(data: any) => {
|
||||||
|
const { top, bottom, width } = data.offsets.reference;
|
||||||
|
|
||||||
|
if (enforceMaxHeight) {
|
||||||
|
data.styles.maxHeight = maxHeight;
|
||||||
|
} else {
|
||||||
|
const windowHeight = window.innerHeight;
|
||||||
|
|
||||||
|
if (/^botton/.test(data.placement)) {
|
||||||
|
data.styles.maxHeight = windowHeight - bottom;
|
||||||
|
} else {
|
||||||
|
data.styles.maxHeight = top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data.styles.width = width;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
[enforceMaxHeight, maxHeight]
|
||||||
|
);
|
||||||
|
|
||||||
|
const createRenderInputComponent = useCallback(
|
||||||
|
(inputProps: RenderInputComponentProps) => {
|
||||||
|
return (
|
||||||
|
<Reference>
|
||||||
|
{({ ref }) => {
|
||||||
|
if (renderInputComponent) {
|
||||||
|
return renderInputComponent(inputProps, ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref}>
|
||||||
|
<input {...inputProps} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Reference>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[renderInputComponent]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderSuggestionsContainer = useCallback(
|
||||||
|
({ containerProps, children }: RenderSuggestionsContainerParams) => {
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<Popper
|
||||||
|
placement="bottom-start"
|
||||||
|
modifiers={{
|
||||||
|
computeMaxHeight: {
|
||||||
|
order: 851,
|
||||||
|
enabled: true,
|
||||||
|
fn: handleComputeMaxHeight,
|
||||||
|
},
|
||||||
|
flip: {
|
||||||
|
padding: minHeight,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ ref: popperRef, style, scheduleUpdate }) => {
|
||||||
|
updater.current = scheduleUpdate;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={popperRef}
|
||||||
|
style={style}
|
||||||
|
className={
|
||||||
|
children ? styles.suggestionsContainerOpen : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
{...containerProps}
|
||||||
|
style={{
|
||||||
|
maxHeight: style.maxHeight,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Popper>
|
||||||
|
</Portal>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[minHeight, handleComputeMaxHeight]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleInputKeyDown = useCallback(
|
||||||
|
(event: KeyboardEvent<HTMLElement>) => {
|
||||||
|
if (
|
||||||
|
event.key === 'Tab' &&
|
||||||
|
suggestions.length &&
|
||||||
|
suggestions[0] !== value
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
onSuggestionSelected?.(event, {
|
||||||
|
suggestion: suggestions[0],
|
||||||
|
suggestionValue: value,
|
||||||
|
suggestionIndex: 0,
|
||||||
|
sectionIndex: null,
|
||||||
|
method: 'enter',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[value, suggestions, onSuggestionSelected]
|
||||||
|
);
|
||||||
|
|
||||||
|
const inputProps = {
|
||||||
|
className: classNames(
|
||||||
|
className,
|
||||||
|
hasError && styles.hasError,
|
||||||
|
hasWarning && styles.hasWarning
|
||||||
|
),
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
placeholder,
|
||||||
|
autoComplete: 'off',
|
||||||
|
spellCheck: false,
|
||||||
|
onChange: onInputChange,
|
||||||
|
onKeyDown: onInputKeyDown || handleInputKeyDown,
|
||||||
|
onFocus: onInputFocus,
|
||||||
|
onBlur: onInputBlur,
|
||||||
|
};
|
||||||
|
|
||||||
|
const theme = {
|
||||||
|
container: inputContainerClassName,
|
||||||
|
containerOpen: styles.suggestionsContainerOpen,
|
||||||
|
suggestionsContainer: styles.suggestionsContainer,
|
||||||
|
suggestionsList: styles.suggestionsList,
|
||||||
|
suggestion: styles.suggestion,
|
||||||
|
suggestionHighlighted: styles.suggestionHighlighted,
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (updater.current && suggestions !== previousSuggestions) {
|
||||||
|
updater.current();
|
||||||
|
}
|
||||||
|
}, [suggestions, previousSuggestions]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Manager>
|
||||||
|
<Autosuggest
|
||||||
|
{...otherProps}
|
||||||
|
ref={forwardedRef}
|
||||||
|
id={name}
|
||||||
|
inputProps={inputProps}
|
||||||
|
theme={theme}
|
||||||
|
suggestions={suggestions}
|
||||||
|
getSuggestionValue={getSuggestionValue}
|
||||||
|
renderInputComponent={createRenderInputComponent}
|
||||||
|
renderSuggestionsContainer={renderSuggestionsContainer}
|
||||||
|
renderSuggestion={renderSuggestion}
|
||||||
|
onSuggestionSelected={onSuggestionSelected}
|
||||||
|
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
|
||||||
|
onSuggestionsClearRequested={onSuggestionsClearRequested}
|
||||||
|
/>
|
||||||
|
</Manager>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AutoSuggestInput;
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import ReCAPTCHA from 'react-google-recaptcha';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import FormInputButton from './FormInputButton';
|
|
||||||
import TextInput from './TextInput';
|
|
||||||
import styles from './CaptchaInput.css';
|
|
||||||
|
|
||||||
function CaptchaInput(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
hasError,
|
|
||||||
hasWarning,
|
|
||||||
refreshing,
|
|
||||||
siteKey,
|
|
||||||
secretToken,
|
|
||||||
onChange,
|
|
||||||
onRefreshPress,
|
|
||||||
onCaptchaChange
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className={styles.captchaInputWrapper}>
|
|
||||||
<TextInput
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
styles.hasButton,
|
|
||||||
hasError && styles.hasError,
|
|
||||||
hasWarning && styles.hasWarning
|
|
||||||
)}
|
|
||||||
name={name}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormInputButton
|
|
||||||
onPress={onRefreshPress}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name={icons.REFRESH}
|
|
||||||
isSpinning={refreshing}
|
|
||||||
/>
|
|
||||||
</FormInputButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
!!siteKey && !!secretToken &&
|
|
||||||
<div className={styles.recaptchaWrapper}>
|
|
||||||
<ReCAPTCHA
|
|
||||||
sitekey={siteKey}
|
|
||||||
stoken={secretToken}
|
|
||||||
onChange={onCaptchaChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
CaptchaInput.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.string.isRequired,
|
|
||||||
hasError: PropTypes.bool,
|
|
||||||
hasWarning: PropTypes.bool,
|
|
||||||
refreshing: PropTypes.bool.isRequired,
|
|
||||||
siteKey: PropTypes.string,
|
|
||||||
secretToken: PropTypes.string,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
onRefreshPress: PropTypes.func.isRequired,
|
|
||||||
onCaptchaChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
CaptchaInput.defaultProps = {
|
|
||||||
className: styles.input,
|
|
||||||
value: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CaptchaInput;
|
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useCallback, useEffect } from 'react';
|
||||||
|
import ReCAPTCHA from 'react-google-recaptcha';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import {
|
||||||
|
getCaptchaCookie,
|
||||||
|
refreshCaptcha,
|
||||||
|
resetCaptcha,
|
||||||
|
} from 'Store/Actions/captchaActions';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import FormInputButton from './FormInputButton';
|
||||||
|
import TextInput from './TextInput';
|
||||||
|
import styles from './CaptchaInput.css';
|
||||||
|
|
||||||
|
interface CaptchaInputProps {
|
||||||
|
className?: string;
|
||||||
|
name: string;
|
||||||
|
value?: string;
|
||||||
|
provider: string;
|
||||||
|
providerData: object;
|
||||||
|
hasError?: boolean;
|
||||||
|
hasWarning?: boolean;
|
||||||
|
refreshing: boolean;
|
||||||
|
siteKey?: string;
|
||||||
|
secretToken?: string;
|
||||||
|
onChange: (change: InputChanged<string>) => unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CaptchaInput({
|
||||||
|
className = styles.input,
|
||||||
|
name,
|
||||||
|
value = '',
|
||||||
|
provider,
|
||||||
|
providerData,
|
||||||
|
hasError,
|
||||||
|
hasWarning,
|
||||||
|
refreshing,
|
||||||
|
siteKey,
|
||||||
|
secretToken,
|
||||||
|
onChange,
|
||||||
|
}: CaptchaInputProps) {
|
||||||
|
const { token } = useSelector((state: AppState) => state.captcha);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const previousToken = usePrevious(token);
|
||||||
|
|
||||||
|
const handleCaptchaChange = useCallback(
|
||||||
|
(token: string | null) => {
|
||||||
|
// If the captcha has expired `captchaResponse` will be null.
|
||||||
|
// In the event it's null don't try to get the captchaCookie.
|
||||||
|
// TODO: Should we clear the cookie? or reset the captcha?
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
getCaptchaCookie({
|
||||||
|
provider,
|
||||||
|
providerData,
|
||||||
|
captchaResponse: token,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[provider, providerData, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRefreshPress = useCallback(() => {
|
||||||
|
dispatch(refreshCaptcha({ provider, providerData }));
|
||||||
|
}, [provider, providerData, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (token && token !== previousToken) {
|
||||||
|
onChange({ name, value: token });
|
||||||
|
}
|
||||||
|
}, [name, token, previousToken, onChange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(resetCaptcha());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={styles.captchaInputWrapper}>
|
||||||
|
<TextInput
|
||||||
|
className={classNames(
|
||||||
|
className,
|
||||||
|
styles.hasButton,
|
||||||
|
hasError && styles.hasError,
|
||||||
|
hasWarning && styles.hasWarning
|
||||||
|
)}
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormInputButton onPress={handleRefreshPress}>
|
||||||
|
<Icon name={icons.REFRESH} isSpinning={refreshing} />
|
||||||
|
</FormInputButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{siteKey && secretToken ? (
|
||||||
|
<div className={styles.recaptchaWrapper}>
|
||||||
|
<ReCAPTCHA
|
||||||
|
sitekey={siteKey}
|
||||||
|
stoken={secretToken}
|
||||||
|
onChange={handleCaptchaChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CaptchaInput;
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { getCaptchaCookie, refreshCaptcha, resetCaptcha } from 'Store/Actions/captchaActions';
|
|
||||||
import CaptchaInput from './CaptchaInput';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.captcha,
|
|
||||||
(captcha) => {
|
|
||||||
return captcha;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
refreshCaptcha,
|
|
||||||
getCaptchaCookie,
|
|
||||||
resetCaptcha
|
|
||||||
};
|
|
||||||
|
|
||||||
class CaptchaInputConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
token,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (token && token !== prevProps.token) {
|
|
||||||
onChange({ name, value: token });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount = () => {
|
|
||||||
this.props.resetCaptcha();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onRefreshPress = () => {
|
|
||||||
const {
|
|
||||||
provider,
|
|
||||||
providerData
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
this.props.refreshCaptcha({ provider, providerData });
|
|
||||||
};
|
|
||||||
|
|
||||||
onCaptchaChange = (captchaResponse) => {
|
|
||||||
// If the captcha has expired `captchaResponse` will be null.
|
|
||||||
// In the event it's null don't try to get the captchaCookie.
|
|
||||||
// TODO: Should we clear the cookie? or reset the captcha?
|
|
||||||
|
|
||||||
if (!captchaResponse) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
provider,
|
|
||||||
providerData
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
this.props.getCaptchaCookie({ provider, providerData, captchaResponse });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<CaptchaInput
|
|
||||||
{...this.props}
|
|
||||||
onRefreshPress={this.onRefreshPress}
|
|
||||||
onCaptchaChange={this.onCaptchaChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CaptchaInputConnector.propTypes = {
|
|
||||||
provider: PropTypes.string.isRequired,
|
|
||||||
providerData: PropTypes.object.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
token: PropTypes.string,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
refreshCaptcha: PropTypes.func.isRequired,
|
|
||||||
getCaptchaCookie: PropTypes.func.isRequired,
|
|
||||||
resetCaptcha: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(CaptchaInputConnector);
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import FormInputHelpText from './FormInputHelpText';
|
|
||||||
import styles from './CheckInput.css';
|
|
||||||
|
|
||||||
class CheckInput extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._checkbox = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.setIndeterminate();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
this.setIndeterminate();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
setIndeterminate() {
|
|
||||||
if (!this._checkbox) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
value,
|
|
||||||
uncheckedValue,
|
|
||||||
checkedValue
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
this._checkbox.indeterminate = value !== uncheckedValue && value !== checkedValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleChecked = (checked, shiftKey) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
checkedValue,
|
|
||||||
uncheckedValue
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const newValue = checked ? checkedValue : uncheckedValue;
|
|
||||||
|
|
||||||
if (value !== newValue) {
|
|
||||||
this.props.onChange({
|
|
||||||
name,
|
|
||||||
value: newValue,
|
|
||||||
shiftKey
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
setRef = (ref) => {
|
|
||||||
this._checkbox = ref;
|
|
||||||
};
|
|
||||||
|
|
||||||
onClick = (event) => {
|
|
||||||
if (this.props.isDisabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shiftKey = event.nativeEvent.shiftKey;
|
|
||||||
const checked = !this._checkbox.checked;
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
this.toggleChecked(checked, shiftKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
onChange = (event) => {
|
|
||||||
const checked = event.target.checked;
|
|
||||||
const shiftKey = event.nativeEvent.shiftKey;
|
|
||||||
|
|
||||||
this.toggleChecked(checked, shiftKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
containerClassName,
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
checkedValue,
|
|
||||||
uncheckedValue,
|
|
||||||
helpText,
|
|
||||||
helpTextWarning,
|
|
||||||
isDisabled,
|
|
||||||
kind
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const isChecked = value === checkedValue;
|
|
||||||
const isUnchecked = value === uncheckedValue;
|
|
||||||
const isIndeterminate = !isChecked && !isUnchecked;
|
|
||||||
const isCheckClass = `${kind}IsChecked`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={containerClassName}>
|
|
||||||
<label
|
|
||||||
className={styles.label}
|
|
||||||
onClick={this.onClick}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
ref={this.setRef}
|
|
||||||
className={styles.checkbox}
|
|
||||||
type="checkbox"
|
|
||||||
name={name}
|
|
||||||
checked={isChecked}
|
|
||||||
disabled={isDisabled}
|
|
||||||
onChange={this.onChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
isChecked ? styles[isCheckClass] : styles.isNotChecked,
|
|
||||||
isIndeterminate && styles.isIndeterminate,
|
|
||||||
isDisabled && styles.isDisabled
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
isChecked &&
|
|
||||||
<Icon name={icons.CHECK} />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isIndeterminate &&
|
|
||||||
<Icon name={icons.CHECK_INDETERMINATE} />
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
helpText &&
|
|
||||||
<FormInputHelpText
|
|
||||||
className={styles.helpText}
|
|
||||||
text={helpText}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!helpText && helpTextWarning &&
|
|
||||||
<FormInputHelpText
|
|
||||||
className={styles.helpText}
|
|
||||||
text={helpTextWarning}
|
|
||||||
isWarning={true}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckInput.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
containerClassName: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
checkedValue: PropTypes.bool,
|
|
||||||
uncheckedValue: PropTypes.bool,
|
|
||||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
|
|
||||||
helpText: PropTypes.string,
|
|
||||||
helpTextWarning: PropTypes.string,
|
|
||||||
isDisabled: PropTypes.bool,
|
|
||||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
CheckInput.defaultProps = {
|
|
||||||
className: styles.input,
|
|
||||||
containerClassName: styles.container,
|
|
||||||
checkedValue: true,
|
|
||||||
uncheckedValue: false,
|
|
||||||
kind: kinds.PRIMARY
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CheckInput;
|
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { SyntheticEvent, useCallback, useEffect, useRef } from 'react';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import { Kind } from 'Helpers/Props/kinds';
|
||||||
|
import { CheckInputChanged } from 'typings/inputs';
|
||||||
|
import FormInputHelpText from './FormInputHelpText';
|
||||||
|
import styles from './CheckInput.css';
|
||||||
|
|
||||||
|
interface ChangeEvent<T = Element> extends SyntheticEvent<T, MouseEvent> {
|
||||||
|
target: EventTarget & T;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CheckInputProps {
|
||||||
|
className?: string;
|
||||||
|
containerClassName?: string;
|
||||||
|
name: string;
|
||||||
|
checkedValue?: boolean;
|
||||||
|
uncheckedValue?: boolean;
|
||||||
|
value?: string | boolean;
|
||||||
|
helpText?: string;
|
||||||
|
helpTextWarning?: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
kind?: Extract<Kind, keyof typeof styles>;
|
||||||
|
onChange: (changes: CheckInputChanged) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CheckInput(props: CheckInputProps) {
|
||||||
|
const {
|
||||||
|
className = styles.input,
|
||||||
|
containerClassName = styles.container,
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
checkedValue = true,
|
||||||
|
uncheckedValue = false,
|
||||||
|
helpText,
|
||||||
|
helpTextWarning,
|
||||||
|
isDisabled,
|
||||||
|
kind = 'primary',
|
||||||
|
onChange,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const isChecked = value === checkedValue;
|
||||||
|
const isUnchecked = value === uncheckedValue;
|
||||||
|
const isIndeterminate = !isChecked && !isUnchecked;
|
||||||
|
const isCheckClass: keyof typeof styles = `${kind}IsChecked`;
|
||||||
|
|
||||||
|
const toggleChecked = useCallback(
|
||||||
|
(checked: boolean, shiftKey: boolean) => {
|
||||||
|
const newValue = checked ? checkedValue : uncheckedValue;
|
||||||
|
|
||||||
|
if (value !== newValue) {
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: newValue,
|
||||||
|
shiftKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[name, value, checkedValue, uncheckedValue, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClick = useCallback(
|
||||||
|
(event: SyntheticEvent<HTMLElement, MouseEvent>) => {
|
||||||
|
if (isDisabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shiftKey = event.nativeEvent.shiftKey;
|
||||||
|
const checked = !(inputRef.current?.checked ?? false);
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
toggleChecked(checked, shiftKey);
|
||||||
|
},
|
||||||
|
[isDisabled, toggleChecked]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const checked = event.target.checked;
|
||||||
|
const shiftKey = event.nativeEvent.shiftKey;
|
||||||
|
|
||||||
|
toggleChecked(checked, shiftKey);
|
||||||
|
},
|
||||||
|
[toggleChecked]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!inputRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
inputRef.current.indeterminate =
|
||||||
|
value !== uncheckedValue && value !== checkedValue;
|
||||||
|
}, [value, uncheckedValue, checkedValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={containerClassName}>
|
||||||
|
<label className={styles.label} onClick={handleClick}>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
className={styles.checkbox}
|
||||||
|
type="checkbox"
|
||||||
|
name={name}
|
||||||
|
checked={isChecked}
|
||||||
|
disabled={isDisabled}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
className,
|
||||||
|
isChecked ? styles[isCheckClass] : styles.isNotChecked,
|
||||||
|
isIndeterminate && styles.isIndeterminate,
|
||||||
|
isDisabled && styles.isDisabled
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isChecked ? <Icon name={icons.CHECK} /> : null}
|
||||||
|
|
||||||
|
{isIndeterminate ? <Icon name={icons.CHECK_INDETERMINATE} /> : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{helpText ? (
|
||||||
|
<FormInputHelpText className={styles.helpText} text={helpText} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!helpText && helpTextWarning ? (
|
||||||
|
<FormInputHelpText
|
||||||
|
className={styles.helpText}
|
||||||
|
text={helpTextWarning}
|
||||||
|
isWarning={true}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CheckInput;
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import tagShape from 'Helpers/Props/Shapes/tagShape';
|
|
||||||
import FormInputButton from './FormInputButton';
|
|
||||||
import TagInput from './TagInput';
|
|
||||||
import styles from './DeviceInput.css';
|
|
||||||
|
|
||||||
class DeviceInput extends Component {
|
|
||||||
|
|
||||||
onTagAdd = (device) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
// New tags won't have an ID, only a name.
|
|
||||||
const deviceId = device.id || device.name;
|
|
||||||
|
|
||||||
onChange({
|
|
||||||
name,
|
|
||||||
value: [...value, deviceId]
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onTagDelete = ({ index }) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const newValue = value.slice();
|
|
||||||
newValue.splice(index, 1);
|
|
||||||
|
|
||||||
onChange({
|
|
||||||
name,
|
|
||||||
value: newValue
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
name,
|
|
||||||
items,
|
|
||||||
selectedDevices,
|
|
||||||
hasError,
|
|
||||||
hasWarning,
|
|
||||||
isFetching,
|
|
||||||
onRefreshPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
<TagInput
|
|
||||||
inputContainerClassName={styles.input}
|
|
||||||
name={name}
|
|
||||||
tags={selectedDevices}
|
|
||||||
tagList={items}
|
|
||||||
allowNew={true}
|
|
||||||
minQueryLength={0}
|
|
||||||
hasError={hasError}
|
|
||||||
hasWarning={hasWarning}
|
|
||||||
onTagAdd={this.onTagAdd}
|
|
||||||
onTagDelete={this.onTagDelete}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormInputButton
|
|
||||||
onPress={onRefreshPress}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name={icons.REFRESH}
|
|
||||||
isSpinning={isFetching}
|
|
||||||
/>
|
|
||||||
</FormInputButton>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DeviceInput.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])).isRequired,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
|
||||||
selectedDevices: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
|
||||||
hasError: PropTypes.bool,
|
|
||||||
hasWarning: PropTypes.bool,
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
onRefreshPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
DeviceInput.defaultProps = {
|
|
||||||
className: styles.deviceInputWrapper,
|
|
||||||
inputClassName: styles.input
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DeviceInput;
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { clearOptions, defaultState, fetchOptions } from 'Store/Actions/providerOptionActions';
|
|
||||||
import DeviceInput from './DeviceInput';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state, { value }) => value,
|
|
||||||
(state) => state.providerOptions.devices || defaultState,
|
|
||||||
(value, devices) => {
|
|
||||||
|
|
||||||
return {
|
|
||||||
...devices,
|
|
||||||
selectedDevices: value.map((valueDevice) => {
|
|
||||||
// Disable equality ESLint rule so we don't need to worry about
|
|
||||||
// a type mismatch between the value items and the device ID.
|
|
||||||
// eslint-disable-next-line eqeqeq
|
|
||||||
const device = devices.items.find((d) => d.id == valueDevice);
|
|
||||||
|
|
||||||
if (device) {
|
|
||||||
return {
|
|
||||||
id: device.id,
|
|
||||||
name: `${device.name} (${device.id})`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: valueDevice,
|
|
||||||
name: `Unknown (${valueDevice})`
|
|
||||||
};
|
|
||||||
})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchFetchOptions: fetchOptions,
|
|
||||||
dispatchClearOptions: clearOptions
|
|
||||||
};
|
|
||||||
|
|
||||||
class DeviceInputConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount = () => {
|
|
||||||
this._populate();
|
|
||||||
};
|
|
||||||
|
|
||||||
componentWillUnmount = () => {
|
|
||||||
this.props.dispatchClearOptions({ section: 'devices' });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
_populate() {
|
|
||||||
const {
|
|
||||||
provider,
|
|
||||||
providerData,
|
|
||||||
dispatchFetchOptions
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
dispatchFetchOptions({
|
|
||||||
section: 'devices',
|
|
||||||
action: 'getDevices',
|
|
||||||
provider,
|
|
||||||
providerData
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onRefreshPress = () => {
|
|
||||||
this._populate();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<DeviceInput
|
|
||||||
{...this.props}
|
|
||||||
onRefreshPress={this.onRefreshPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DeviceInputConnector.propTypes = {
|
|
||||||
provider: PropTypes.string.isRequired,
|
|
||||||
providerData: PropTypes.object.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
dispatchFetchOptions: PropTypes.func.isRequired,
|
|
||||||
dispatchClearOptions: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(DeviceInputConnector);
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.settings.downloadClients,
|
|
||||||
(state, { includeAny }) => includeAny,
|
|
||||||
(state, { protocol }) => protocol,
|
|
||||||
(downloadClients, includeAny, protocolFilter) => {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items
|
|
||||||
} = downloadClients;
|
|
||||||
|
|
||||||
const filteredItems = items.filter((item) => item.protocol === protocolFilter);
|
|
||||||
|
|
||||||
const values = _.map(filteredItems.sort(sortByProp('name')), (downloadClient) => {
|
|
||||||
return {
|
|
||||||
key: downloadClient.id,
|
|
||||||
value: downloadClient.name,
|
|
||||||
hint: `(${downloadClient.id})`
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (includeAny) {
|
|
||||||
values.unshift({
|
|
||||||
key: 0,
|
|
||||||
value: `(${translate('Any')})`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
values
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchFetchDownloadClients: fetchDownloadClients
|
|
||||||
};
|
|
||||||
|
|
||||||
class DownloadClientSelectInputConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
if (!this.props.isPopulated) {
|
|
||||||
this.props.dispatchFetchDownloadClients();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onChange = ({ name, value }) => {
|
|
||||||
this.props.onChange({ name, value: parseInt(value) });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<EnhancedSelectInput
|
|
||||||
{...this.props}
|
|
||||||
onChange={this.onChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DownloadClientSelectInputConnector.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
|
||||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
includeAny: PropTypes.bool.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
dispatchFetchDownloadClients: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
DownloadClientSelectInputConnector.defaultProps = {
|
|
||||||
includeAny: false,
|
|
||||||
protocol: 'torrent'
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(DownloadClientSelectInputConnector);
|
|
||||||
@@ -1,614 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { Manager, Popper, Reference } from 'react-popper';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import Measure from 'Components/Measure';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import Portal from 'Components/Portal';
|
|
||||||
import Scroller from 'Components/Scroller/Scroller';
|
|
||||||
import { icons, scrollDirections, sizes } from 'Helpers/Props';
|
|
||||||
import { isMobile as isMobileUtil } from 'Utilities/browser';
|
|
||||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
|
||||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
|
||||||
import HintedSelectInputOption from './HintedSelectInputOption';
|
|
||||||
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
|
||||||
import TextInput from './TextInput';
|
|
||||||
import styles from './EnhancedSelectInput.css';
|
|
||||||
|
|
||||||
function isArrowKey(keyCode) {
|
|
||||||
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSelectedOption(selectedIndex, values) {
|
|
||||||
return values[selectedIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
function findIndex(startingIndex, direction, values) {
|
|
||||||
let indexToTest = startingIndex + direction;
|
|
||||||
|
|
||||||
while (indexToTest !== startingIndex) {
|
|
||||||
if (indexToTest < 0) {
|
|
||||||
indexToTest = values.length - 1;
|
|
||||||
} else if (indexToTest >= values.length) {
|
|
||||||
indexToTest = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getSelectedOption(indexToTest, values).isDisabled) {
|
|
||||||
indexToTest = indexToTest + direction;
|
|
||||||
} else {
|
|
||||||
return indexToTest;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function previousIndex(selectedIndex, values) {
|
|
||||||
return findIndex(selectedIndex, -1, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
function nextIndex(selectedIndex, values) {
|
|
||||||
return findIndex(selectedIndex, 1, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSelectedIndex(props) {
|
|
||||||
const {
|
|
||||||
value,
|
|
||||||
values
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return values.findIndex((v) => {
|
|
||||||
return value.size && v.key === value[0];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return values.findIndex((v) => {
|
|
||||||
return v.key === value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSelectedItem(index, props) {
|
|
||||||
const {
|
|
||||||
value,
|
|
||||||
values
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value.includes(values[index].key);
|
|
||||||
}
|
|
||||||
|
|
||||||
return values[index].key === value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getKey(selectedIndex, values) {
|
|
||||||
return values[selectedIndex].key;
|
|
||||||
}
|
|
||||||
|
|
||||||
class EnhancedSelectInput extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._scheduleUpdate = null;
|
|
||||||
this._buttonId = getUniqueElememtId();
|
|
||||||
this._optionsId = getUniqueElememtId();
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isOpen: false,
|
|
||||||
selectedIndex: getSelectedIndex(props),
|
|
||||||
width: 0,
|
|
||||||
isMobile: isMobileUtil()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (this._scheduleUpdate) {
|
|
||||||
this._scheduleUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(this.props.value)) {
|
|
||||||
if (prevProps.value !== this.props.value || prevProps.values !== this.props.values) {
|
|
||||||
this.setState({
|
|
||||||
selectedIndex: getSelectedIndex(this.props)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
_addListener() {
|
|
||||||
window.addEventListener('click', this.onWindowClick);
|
|
||||||
}
|
|
||||||
|
|
||||||
_removeListener() {
|
|
||||||
window.removeEventListener('click', this.onWindowClick);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onComputeMaxHeight = (data) => {
|
|
||||||
const {
|
|
||||||
top,
|
|
||||||
bottom
|
|
||||||
} = data.offsets.reference;
|
|
||||||
|
|
||||||
const windowHeight = window.innerHeight;
|
|
||||||
|
|
||||||
if ((/^botton/).test(data.placement)) {
|
|
||||||
data.styles.maxHeight = windowHeight - bottom;
|
|
||||||
} else {
|
|
||||||
data.styles.maxHeight = top;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
onWindowClick = (event) => {
|
|
||||||
const button = document.getElementById(this._buttonId);
|
|
||||||
const options = document.getElementById(this._optionsId);
|
|
||||||
|
|
||||||
if (!button || !event.target.isConnected || this.state.isMobile) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!button.contains(event.target) &&
|
|
||||||
options &&
|
|
||||||
!options.contains(event.target) &&
|
|
||||||
this.state.isOpen
|
|
||||||
) {
|
|
||||||
this.setState({ isOpen: false });
|
|
||||||
this._removeListener();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onFocus = () => {
|
|
||||||
if (this.state.isOpen) {
|
|
||||||
this._removeListener();
|
|
||||||
this.setState({ isOpen: false });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onBlur = () => {
|
|
||||||
if (!this.props.isEditable) {
|
|
||||||
// Calling setState without this check prevents the click event from being properly handled on Chrome (it is on firefox)
|
|
||||||
const origIndex = getSelectedIndex(this.props);
|
|
||||||
|
|
||||||
if (origIndex !== this.state.selectedIndex) {
|
|
||||||
this.setState({ selectedIndex: origIndex });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onKeyDown = (event) => {
|
|
||||||
const {
|
|
||||||
values
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
selectedIndex
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const keyCode = event.keyCode;
|
|
||||||
const newState = {};
|
|
||||||
|
|
||||||
if (!isOpen) {
|
|
||||||
if (isArrowKey(keyCode)) {
|
|
||||||
event.preventDefault();
|
|
||||||
newState.isOpen = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
selectedIndex == null || selectedIndex === -1 ||
|
|
||||||
getSelectedOption(selectedIndex, values).isDisabled
|
|
||||||
) {
|
|
||||||
if (keyCode === keyCodes.UP_ARROW) {
|
|
||||||
newState.selectedIndex = previousIndex(0, values);
|
|
||||||
} else if (keyCode === keyCodes.DOWN_ARROW) {
|
|
||||||
newState.selectedIndex = nextIndex(values.length - 1, values);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState(newState);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyCode === keyCodes.UP_ARROW) {
|
|
||||||
event.preventDefault();
|
|
||||||
newState.selectedIndex = previousIndex(selectedIndex, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyCode === keyCodes.DOWN_ARROW) {
|
|
||||||
event.preventDefault();
|
|
||||||
newState.selectedIndex = nextIndex(selectedIndex, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyCode === keyCodes.ENTER) {
|
|
||||||
event.preventDefault();
|
|
||||||
newState.isOpen = false;
|
|
||||||
this.onSelect(getKey(selectedIndex, values));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyCode === keyCodes.TAB) {
|
|
||||||
newState.isOpen = false;
|
|
||||||
this.onSelect(getKey(selectedIndex, values));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyCode === keyCodes.ESCAPE) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
newState.isOpen = false;
|
|
||||||
newState.selectedIndex = getSelectedIndex(this.props);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_.isEmpty(newState)) {
|
|
||||||
this.setState(newState);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onPress = () => {
|
|
||||||
if (this.state.isOpen) {
|
|
||||||
this._removeListener();
|
|
||||||
} else {
|
|
||||||
this._addListener();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.state.isOpen && this.props.onOpen) {
|
|
||||||
this.props.onOpen();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ isOpen: !this.state.isOpen });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSelect = (newValue) => {
|
|
||||||
const { name, value, values, onChange } = this.props;
|
|
||||||
const additionalProperties = values.find((v) => v.key === newValue)?.additionalProperties;
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
let arrayValue = null;
|
|
||||||
const index = value.indexOf(newValue);
|
|
||||||
|
|
||||||
if (index === -1) {
|
|
||||||
arrayValue = values.map((v) => v.key).filter((v) => (v === newValue) || value.includes(v));
|
|
||||||
} else {
|
|
||||||
arrayValue = [...value];
|
|
||||||
arrayValue.splice(index, 1);
|
|
||||||
}
|
|
||||||
onChange({
|
|
||||||
name,
|
|
||||||
value: arrayValue,
|
|
||||||
additionalProperties
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState({ isOpen: false });
|
|
||||||
|
|
||||||
onChange({
|
|
||||||
name,
|
|
||||||
value: newValue,
|
|
||||||
additionalProperties
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMeasure = ({ width }) => {
|
|
||||||
this.setState({ width });
|
|
||||||
};
|
|
||||||
|
|
||||||
onOptionsModalClose = () => {
|
|
||||||
this.setState({ isOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
disabledClassName,
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
values,
|
|
||||||
isDisabled,
|
|
||||||
isEditable,
|
|
||||||
isFetching,
|
|
||||||
hasError,
|
|
||||||
hasWarning,
|
|
||||||
valueOptions,
|
|
||||||
selectedValueOptions,
|
|
||||||
selectedValueComponent: SelectedValueComponent,
|
|
||||||
optionComponent: OptionComponent,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
selectedIndex,
|
|
||||||
width,
|
|
||||||
isOpen,
|
|
||||||
isMobile
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const isMultiSelect = Array.isArray(value);
|
|
||||||
const selectedOption = getSelectedOption(selectedIndex, values);
|
|
||||||
let selectedValue = value;
|
|
||||||
|
|
||||||
if (!values.length) {
|
|
||||||
selectedValue = isMultiSelect ? [] : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Manager>
|
|
||||||
<Reference>
|
|
||||||
{({ ref }) => (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
id={this._buttonId}
|
|
||||||
>
|
|
||||||
<Measure
|
|
||||||
whitelist={['width']}
|
|
||||||
onMeasure={this.onMeasure}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
isEditable ?
|
|
||||||
<div
|
|
||||||
className={styles.editableContainer}
|
|
||||||
>
|
|
||||||
<TextInput
|
|
||||||
className={className}
|
|
||||||
name={name}
|
|
||||||
value={value}
|
|
||||||
readOnly={isDisabled}
|
|
||||||
hasError={hasError}
|
|
||||||
hasWarning={hasWarning}
|
|
||||||
onFocus={this.onFocus}
|
|
||||||
onBlur={this.onBlur}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
<Link
|
|
||||||
className={classNames(
|
|
||||||
styles.dropdownArrowContainerEditable,
|
|
||||||
isDisabled ?
|
|
||||||
styles.dropdownArrowContainerDisabled :
|
|
||||||
styles.dropdownArrowContainer)
|
|
||||||
}
|
|
||||||
onPress={this.onPress}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
isFetching ?
|
|
||||||
<LoadingIndicator
|
|
||||||
className={styles.loading}
|
|
||||||
size={20}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isFetching ?
|
|
||||||
null :
|
|
||||||
<Icon
|
|
||||||
name={icons.CARET_DOWN}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</Link>
|
|
||||||
</div> :
|
|
||||||
<Link
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
hasError && styles.hasError,
|
|
||||||
hasWarning && styles.hasWarning,
|
|
||||||
isDisabled && disabledClassName
|
|
||||||
)}
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
onBlur={this.onBlur}
|
|
||||||
onKeyDown={this.onKeyDown}
|
|
||||||
onPress={this.onPress}
|
|
||||||
>
|
|
||||||
<SelectedValueComponent
|
|
||||||
value={selectedValue}
|
|
||||||
values={values}
|
|
||||||
{...selectedValueOptions}
|
|
||||||
{...selectedOption}
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
isMultiSelect={isMultiSelect}
|
|
||||||
>
|
|
||||||
{selectedOption ? selectedOption.value : null}
|
|
||||||
</SelectedValueComponent>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={isDisabled ?
|
|
||||||
styles.dropdownArrowContainerDisabled :
|
|
||||||
styles.dropdownArrowContainer
|
|
||||||
}
|
|
||||||
>
|
|
||||||
|
|
||||||
{
|
|
||||||
isFetching ?
|
|
||||||
<LoadingIndicator
|
|
||||||
className={styles.loading}
|
|
||||||
size={20}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isFetching ?
|
|
||||||
null :
|
|
||||||
<Icon
|
|
||||||
name={icons.CARET_DOWN}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
</Measure>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Reference>
|
|
||||||
<Portal>
|
|
||||||
<Popper
|
|
||||||
placement="bottom-start"
|
|
||||||
modifiers={{
|
|
||||||
computeMaxHeight: {
|
|
||||||
order: 851,
|
|
||||||
enabled: true,
|
|
||||||
fn: this.onComputeMaxHeight
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{({ ref, style, scheduleUpdate }) => {
|
|
||||||
this._scheduleUpdate = scheduleUpdate;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
id={this._optionsId}
|
|
||||||
className={styles.optionsContainer}
|
|
||||||
style={{
|
|
||||||
...style,
|
|
||||||
minWidth: width
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
isOpen && !isMobile ?
|
|
||||||
<Scroller
|
|
||||||
className={styles.options}
|
|
||||||
style={{
|
|
||||||
maxHeight: style.maxHeight
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
values.map((v, index) => {
|
|
||||||
const hasParent = v.parentKey !== undefined;
|
|
||||||
const depth = hasParent ? 1 : 0;
|
|
||||||
const parentSelected = hasParent && Array.isArray(value) && value.includes(v.parentKey);
|
|
||||||
return (
|
|
||||||
<OptionComponent
|
|
||||||
key={v.key}
|
|
||||||
id={v.key}
|
|
||||||
depth={depth}
|
|
||||||
isSelected={isSelectedItem(index, this.props)}
|
|
||||||
isDisabled={parentSelected}
|
|
||||||
isMultiSelect={isMultiSelect}
|
|
||||||
{...valueOptions}
|
|
||||||
{...v}
|
|
||||||
isMobile={false}
|
|
||||||
onSelect={this.onSelect}
|
|
||||||
>
|
|
||||||
{v.value}
|
|
||||||
</OptionComponent>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</Scroller> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</Popper>
|
|
||||||
</Portal>
|
|
||||||
</Manager>
|
|
||||||
|
|
||||||
{
|
|
||||||
isMobile ?
|
|
||||||
<Modal
|
|
||||||
className={styles.optionsModal}
|
|
||||||
size={sizes.EXTRA_SMALL}
|
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={this.onOptionsModalClose}
|
|
||||||
>
|
|
||||||
<ModalBody
|
|
||||||
className={styles.optionsModalBody}
|
|
||||||
innerClassName={styles.optionsInnerModalBody}
|
|
||||||
scrollDirection={scrollDirections.NONE}
|
|
||||||
>
|
|
||||||
<Scroller className={styles.optionsModalScroller}>
|
|
||||||
<div className={styles.mobileCloseButtonContainer}>
|
|
||||||
<Link
|
|
||||||
className={styles.mobileCloseButton}
|
|
||||||
onPress={this.onOptionsModalClose}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name={icons.CLOSE}
|
|
||||||
size={18}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
values.map((v, index) => {
|
|
||||||
const hasParent = v.parentKey !== undefined;
|
|
||||||
const depth = hasParent ? 1 : 0;
|
|
||||||
const parentSelected = hasParent && value.includes(v.parentKey);
|
|
||||||
return (
|
|
||||||
<OptionComponent
|
|
||||||
key={v.key}
|
|
||||||
id={v.key}
|
|
||||||
depth={depth}
|
|
||||||
isSelected={isSelectedItem(index, this.props)}
|
|
||||||
isMultiSelect={isMultiSelect}
|
|
||||||
isDisabled={parentSelected}
|
|
||||||
{...valueOptions}
|
|
||||||
{...v}
|
|
||||||
isMobile={true}
|
|
||||||
onSelect={this.onSelect}
|
|
||||||
>
|
|
||||||
{v.value}
|
|
||||||
</OptionComponent>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</Scroller>
|
|
||||||
</ModalBody>
|
|
||||||
</Modal> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EnhancedSelectInput.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
disabledClassName: PropTypes.string,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.number)]).isRequired,
|
|
||||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
isDisabled: PropTypes.bool.isRequired,
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isEditable: PropTypes.bool.isRequired,
|
|
||||||
hasError: PropTypes.bool,
|
|
||||||
hasWarning: PropTypes.bool,
|
|
||||||
valueOptions: PropTypes.object.isRequired,
|
|
||||||
selectedValueOptions: PropTypes.object.isRequired,
|
|
||||||
selectedValueComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
|
|
||||||
optionComponent: PropTypes.elementType,
|
|
||||||
onOpen: PropTypes.func,
|
|
||||||
onChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
EnhancedSelectInput.defaultProps = {
|
|
||||||
className: styles.enhancedSelect,
|
|
||||||
disabledClassName: styles.isDisabled,
|
|
||||||
isDisabled: false,
|
|
||||||
isFetching: false,
|
|
||||||
isEditable: false,
|
|
||||||
valueOptions: {},
|
|
||||||
selectedValueOptions: {},
|
|
||||||
selectedValueComponent: HintedSelectInputSelectedValue,
|
|
||||||
optionComponent: HintedSelectInputOption
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EnhancedSelectInput;
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { clearOptions, defaultState, fetchOptions } from 'Store/Actions/providerOptionActions';
|
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
|
||||||
|
|
||||||
const importantFieldNames = [
|
|
||||||
'baseUrl',
|
|
||||||
'apiPath',
|
|
||||||
'apiKey',
|
|
||||||
'authToken'
|
|
||||||
];
|
|
||||||
|
|
||||||
function getProviderDataKey(providerData) {
|
|
||||||
if (!providerData || !providerData.fields) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fields = providerData.fields
|
|
||||||
.filter((f) => importantFieldNames.includes(f.name))
|
|
||||||
.map((f) => f.value);
|
|
||||||
|
|
||||||
return fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSelectOptions(items) {
|
|
||||||
if (!items) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return items.map((option) => {
|
|
||||||
return {
|
|
||||||
key: option.value,
|
|
||||||
value: option.name,
|
|
||||||
hint: option.hint,
|
|
||||||
parentKey: option.parentValue,
|
|
||||||
isDisabled: option.isDisabled,
|
|
||||||
additionalProperties: option.additionalProperties
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state, { selectOptionsProviderAction }) => state.providerOptions[selectOptionsProviderAction] || defaultState,
|
|
||||||
(options) => {
|
|
||||||
if (options) {
|
|
||||||
return {
|
|
||||||
isFetching: options.isFetching,
|
|
||||||
values: getSelectOptions(options.items)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchFetchOptions: fetchOptions,
|
|
||||||
dispatchClearOptions: clearOptions
|
|
||||||
};
|
|
||||||
|
|
||||||
class EnhancedSelectInputConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
refetchRequired: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount = () => {
|
|
||||||
this._populate();
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidUpdate = (prevProps) => {
|
|
||||||
const prevKey = getProviderDataKey(prevProps.providerData);
|
|
||||||
const nextKey = getProviderDataKey(this.props.providerData);
|
|
||||||
|
|
||||||
if (!_.isEqual(prevKey, nextKey)) {
|
|
||||||
this.setState({ refetchRequired: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
componentWillUnmount = () => {
|
|
||||||
this._cleanup();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onOpen = () => {
|
|
||||||
if (this.state.refetchRequired) {
|
|
||||||
this._populate();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
_populate() {
|
|
||||||
const {
|
|
||||||
provider,
|
|
||||||
providerData,
|
|
||||||
selectOptionsProviderAction,
|
|
||||||
dispatchFetchOptions
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (selectOptionsProviderAction) {
|
|
||||||
this.setState({ refetchRequired: false });
|
|
||||||
dispatchFetchOptions({
|
|
||||||
section: selectOptionsProviderAction,
|
|
||||||
action: selectOptionsProviderAction,
|
|
||||||
provider,
|
|
||||||
providerData
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_cleanup() {
|
|
||||||
const {
|
|
||||||
selectOptionsProviderAction,
|
|
||||||
dispatchClearOptions
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (selectOptionsProviderAction) {
|
|
||||||
dispatchClearOptions({ section: selectOptionsProviderAction });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<EnhancedSelectInput
|
|
||||||
{...this.props}
|
|
||||||
onOpen={this.onOpen}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EnhancedSelectInputConnector.propTypes = {
|
|
||||||
provider: PropTypes.string.isRequired,
|
|
||||||
providerData: PropTypes.object.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.number)]).isRequired,
|
|
||||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
selectOptionsProviderAction: PropTypes.string,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
dispatchFetchOptions: PropTypes.func.isRequired,
|
|
||||||
dispatchClearOptions: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(EnhancedSelectInputConnector);
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import CheckInput from './CheckInput';
|
|
||||||
import styles from './EnhancedSelectInputOption.css';
|
|
||||||
|
|
||||||
class EnhancedSelectInputOption extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onPress = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
onSelect
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onSelect(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
onCheckPress = () => {
|
|
||||||
// CheckInput requires a handler. Swallow the change event because onPress will already handle it via event propagation.
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
id,
|
|
||||||
depth,
|
|
||||||
isSelected,
|
|
||||||
isDisabled,
|
|
||||||
isHidden,
|
|
||||||
isMultiSelect,
|
|
||||||
isMobile,
|
|
||||||
children
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
isSelected && !isMultiSelect && styles.isSelected,
|
|
||||||
isDisabled && !isMultiSelect && styles.isDisabled,
|
|
||||||
isHidden && styles.isHidden,
|
|
||||||
isMobile && styles.isMobile
|
|
||||||
)}
|
|
||||||
component="div"
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
onPress={this.onPress}
|
|
||||||
>
|
|
||||||
|
|
||||||
{
|
|
||||||
depth !== 0 &&
|
|
||||||
<div style={{ width: `${depth * 20}px` }} />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isMultiSelect &&
|
|
||||||
<CheckInput
|
|
||||||
className={styles.optionCheckInput}
|
|
||||||
containerClassName={styles.optionCheck}
|
|
||||||
name={`select-${id}`}
|
|
||||||
value={isSelected}
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
onChange={this.onCheckPress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{children}
|
|
||||||
|
|
||||||
{
|
|
||||||
isMobile &&
|
|
||||||
<div className={styles.iconContainer}>
|
|
||||||
<Icon
|
|
||||||
name={isSelected ? icons.CHECK_CIRCLE : icons.CIRCLE_OUTLINE}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EnhancedSelectInputOption.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
|
||||||
depth: PropTypes.number.isRequired,
|
|
||||||
isSelected: PropTypes.bool.isRequired,
|
|
||||||
isDisabled: PropTypes.bool.isRequired,
|
|
||||||
isHidden: PropTypes.bool.isRequired,
|
|
||||||
isMultiSelect: PropTypes.bool.isRequired,
|
|
||||||
isMobile: PropTypes.bool.isRequired,
|
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
onSelect: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
EnhancedSelectInputOption.defaultProps = {
|
|
||||||
className: styles.option,
|
|
||||||
depth: 0,
|
|
||||||
isDisabled: false,
|
|
||||||
isHidden: false,
|
|
||||||
isMultiSelect: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EnhancedSelectInputOption;
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import styles from './EnhancedSelectInputSelectedValue.css';
|
|
||||||
|
|
||||||
function EnhancedSelectInputSelectedValue(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
isDisabled
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames(
|
|
||||||
className,
|
|
||||||
isDisabled && styles.isDisabled
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
EnhancedSelectInputSelectedValue.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
children: PropTypes.node,
|
|
||||||
isDisabled: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
EnhancedSelectInputSelectedValue.defaultProps = {
|
|
||||||
className: styles.selectedValue,
|
|
||||||
isDisabled: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EnhancedSelectInputSelectedValue;
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import { kinds } from 'Helpers/Props';
|
|
||||||
import styles from './Form.css';
|
|
||||||
|
|
||||||
function Form(props) {
|
|
||||||
const {
|
|
||||||
children,
|
|
||||||
validationErrors,
|
|
||||||
validationWarnings,
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
validationErrors.length || validationWarnings.length ?
|
|
||||||
<div className={styles.validationFailures}>
|
|
||||||
{
|
|
||||||
validationErrors.map((error, index) => {
|
|
||||||
return (
|
|
||||||
<Alert
|
|
||||||
key={index}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
>
|
|
||||||
{error.errorMessage}
|
|
||||||
</Alert>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
validationWarnings.map((warning, index) => {
|
|
||||||
return (
|
|
||||||
<Alert
|
|
||||||
key={index}
|
|
||||||
kind={kinds.WARNING}
|
|
||||||
>
|
|
||||||
{warning.errorMessage}
|
|
||||||
</Alert>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Form.propTypes = {
|
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
validationErrors: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
validationWarnings: PropTypes.arrayOf(PropTypes.object).isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
Form.defaultProps = {
|
|
||||||
validationErrors: [],
|
|
||||||
validationWarnings: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Form;
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import { ValidationError, ValidationWarning } from 'typings/pending';
|
||||||
|
import styles from './Form.css';
|
||||||
|
|
||||||
|
export interface FormProps {
|
||||||
|
children: ReactNode;
|
||||||
|
validationErrors?: ValidationError[];
|
||||||
|
validationWarnings?: ValidationWarning[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function Form({
|
||||||
|
children,
|
||||||
|
validationErrors = [],
|
||||||
|
validationWarnings = [],
|
||||||
|
}: FormProps) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{validationErrors.length || validationWarnings.length ? (
|
||||||
|
<div className={styles.validationFailures}>
|
||||||
|
{validationErrors.map((error, index) => {
|
||||||
|
return (
|
||||||
|
<Alert key={index} kind={kinds.DANGER}>
|
||||||
|
{error.errorMessage}
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{validationWarnings.map((warning, index) => {
|
||||||
|
return (
|
||||||
|
<Alert key={index} kind={kinds.WARNING}>
|
||||||
|
{warning.errorMessage}
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Form;
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { map } from 'Helpers/elementChildren';
|
|
||||||
import { sizes } from 'Helpers/Props';
|
|
||||||
import styles from './FormGroup.css';
|
|
||||||
|
|
||||||
function FormGroup(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
size,
|
|
||||||
advancedSettings,
|
|
||||||
isAdvanced,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (!advancedSettings && isAdvanced) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const childProps = isAdvanced ? { isAdvanced } : {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
styles[size]
|
|
||||||
)}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
map(children, (child) => {
|
|
||||||
return React.cloneElement(child, childProps);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
FormGroup.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
size: PropTypes.oneOf(sizes.all).isRequired,
|
|
||||||
advancedSettings: PropTypes.bool.isRequired,
|
|
||||||
isAdvanced: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
FormGroup.defaultProps = {
|
|
||||||
className: styles.group,
|
|
||||||
size: sizes.SMALL,
|
|
||||||
advancedSettings: false,
|
|
||||||
isAdvanced: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FormGroup;
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { Children, ComponentPropsWithoutRef, ReactNode } from 'react';
|
||||||
|
import { Size } from 'Helpers/Props/sizes';
|
||||||
|
import styles from './FormGroup.css';
|
||||||
|
|
||||||
|
interface FormGroupProps extends ComponentPropsWithoutRef<'div'> {
|
||||||
|
className?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
size?: Extract<Size, keyof typeof styles>;
|
||||||
|
advancedSettings?: boolean;
|
||||||
|
isAdvanced?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormGroup(props: FormGroupProps) {
|
||||||
|
const {
|
||||||
|
className = styles.group,
|
||||||
|
children,
|
||||||
|
size = 'small',
|
||||||
|
advancedSettings = false,
|
||||||
|
isAdvanced = false,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
if (!advancedSettings && isAdvanced) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const childProps = isAdvanced ? { isAdvanced } : {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames(className, styles[size])} {...otherProps}>
|
||||||
|
{Children.map(children, (child) => {
|
||||||
|
if (!React.isValidElement(child)) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
return React.cloneElement(child, childProps);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FormGroup;
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
|
||||||
import { kinds } from 'Helpers/Props';
|
|
||||||
import styles from './FormInputButton.css';
|
|
||||||
|
|
||||||
function FormInputButton(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
canSpin,
|
|
||||||
isLastButton,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (canSpin) {
|
|
||||||
return (
|
|
||||||
<SpinnerButton
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
!isLastButton && styles.middleButton
|
|
||||||
)}
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
!isLastButton && styles.middleButton
|
|
||||||
)}
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
FormInputButton.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
isLastButton: PropTypes.bool.isRequired,
|
|
||||||
canSpin: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
FormInputButton.defaultProps = {
|
|
||||||
className: styles.button,
|
|
||||||
isLastButton: true,
|
|
||||||
canSpin: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FormInputButton;
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import Button, { ButtonProps } from 'Components/Link/Button';
|
||||||
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import styles from './FormInputButton.css';
|
||||||
|
|
||||||
|
export interface FormInputButtonProps extends ButtonProps {
|
||||||
|
canSpin?: boolean;
|
||||||
|
isLastButton?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormInputButton({
|
||||||
|
className = styles.button,
|
||||||
|
canSpin = false,
|
||||||
|
isLastButton = true,
|
||||||
|
...otherProps
|
||||||
|
}: FormInputButtonProps) {
|
||||||
|
if (canSpin) {
|
||||||
|
return (
|
||||||
|
<SpinnerButton
|
||||||
|
className={classNames(className, !isLastButton && styles.middleButton)}
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={classNames(className, !isLastButton && styles.middleButton)}
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FormInputButton;
|
||||||
@@ -1,307 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import AutoCompleteInput from './AutoCompleteInput';
|
|
||||||
import CaptchaInputConnector from './CaptchaInputConnector';
|
|
||||||
import CheckInput from './CheckInput';
|
|
||||||
import DeviceInputConnector from './DeviceInputConnector';
|
|
||||||
import DownloadClientSelectInputConnector from './DownloadClientSelectInputConnector';
|
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
|
||||||
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
|
||||||
import FormInputHelpText from './FormInputHelpText';
|
|
||||||
import IndexerFlagsSelectInput from './IndexerFlagsSelectInput';
|
|
||||||
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
|
||||||
import KeyValueListInput from './KeyValueListInput';
|
|
||||||
import MonitorEpisodesSelectInput from './MonitorEpisodesSelectInput';
|
|
||||||
import MonitorNewItemsSelectInput from './MonitorNewItemsSelectInput';
|
|
||||||
import NumberInput from './NumberInput';
|
|
||||||
import OAuthInputConnector from './OAuthInputConnector';
|
|
||||||
import PasswordInput from './PasswordInput';
|
|
||||||
import PathInputConnector from './PathInputConnector';
|
|
||||||
import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector';
|
|
||||||
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
|
|
||||||
import SeriesTagInput from './SeriesTagInput';
|
|
||||||
import SeriesTypeSelectInput from './SeriesTypeSelectInput';
|
|
||||||
import TagInputConnector from './TagInputConnector';
|
|
||||||
import TagSelectInputConnector from './TagSelectInputConnector';
|
|
||||||
import TextArea from './TextArea';
|
|
||||||
import TextInput from './TextInput';
|
|
||||||
import TextTagInputConnector from './TextTagInputConnector';
|
|
||||||
import UMaskInput from './UMaskInput';
|
|
||||||
import styles from './FormInputGroup.css';
|
|
||||||
|
|
||||||
function getComponent(type) {
|
|
||||||
switch (type) {
|
|
||||||
case inputTypes.AUTO_COMPLETE:
|
|
||||||
return AutoCompleteInput;
|
|
||||||
|
|
||||||
case inputTypes.CAPTCHA:
|
|
||||||
return CaptchaInputConnector;
|
|
||||||
|
|
||||||
case inputTypes.CHECK:
|
|
||||||
return CheckInput;
|
|
||||||
|
|
||||||
case inputTypes.DEVICE:
|
|
||||||
return DeviceInputConnector;
|
|
||||||
|
|
||||||
case inputTypes.KEY_VALUE_LIST:
|
|
||||||
return KeyValueListInput;
|
|
||||||
|
|
||||||
case inputTypes.MONITOR_EPISODES_SELECT:
|
|
||||||
return MonitorEpisodesSelectInput;
|
|
||||||
|
|
||||||
case inputTypes.MONITOR_NEW_ITEMS_SELECT:
|
|
||||||
return MonitorNewItemsSelectInput;
|
|
||||||
|
|
||||||
case inputTypes.NUMBER:
|
|
||||||
return NumberInput;
|
|
||||||
|
|
||||||
case inputTypes.OAUTH:
|
|
||||||
return OAuthInputConnector;
|
|
||||||
|
|
||||||
case inputTypes.PASSWORD:
|
|
||||||
return PasswordInput;
|
|
||||||
|
|
||||||
case inputTypes.PATH:
|
|
||||||
return PathInputConnector;
|
|
||||||
|
|
||||||
case inputTypes.QUALITY_PROFILE_SELECT:
|
|
||||||
return QualityProfileSelectInputConnector;
|
|
||||||
|
|
||||||
case inputTypes.INDEXER_SELECT:
|
|
||||||
return IndexerSelectInputConnector;
|
|
||||||
|
|
||||||
case inputTypes.INDEXER_FLAGS_SELECT:
|
|
||||||
return IndexerFlagsSelectInput;
|
|
||||||
|
|
||||||
case inputTypes.DOWNLOAD_CLIENT_SELECT:
|
|
||||||
return DownloadClientSelectInputConnector;
|
|
||||||
|
|
||||||
case inputTypes.ROOT_FOLDER_SELECT:
|
|
||||||
return RootFolderSelectInputConnector;
|
|
||||||
|
|
||||||
case inputTypes.SELECT:
|
|
||||||
return EnhancedSelectInput;
|
|
||||||
|
|
||||||
case inputTypes.DYNAMIC_SELECT:
|
|
||||||
return EnhancedSelectInputConnector;
|
|
||||||
|
|
||||||
case inputTypes.SERIES_TAG:
|
|
||||||
return SeriesTagInput;
|
|
||||||
|
|
||||||
case inputTypes.SERIES_TYPE_SELECT:
|
|
||||||
return SeriesTypeSelectInput;
|
|
||||||
|
|
||||||
case inputTypes.TAG:
|
|
||||||
return TagInputConnector;
|
|
||||||
|
|
||||||
case inputTypes.TEXT_AREA:
|
|
||||||
return TextArea;
|
|
||||||
|
|
||||||
case inputTypes.TEXT_TAG:
|
|
||||||
return TextTagInputConnector;
|
|
||||||
|
|
||||||
case inputTypes.TAG_SELECT:
|
|
||||||
return TagSelectInputConnector;
|
|
||||||
|
|
||||||
case inputTypes.UMASK:
|
|
||||||
return UMaskInput;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return TextInput;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function FormInputGroup(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
containerClassName,
|
|
||||||
inputClassName,
|
|
||||||
type,
|
|
||||||
unit,
|
|
||||||
buttons,
|
|
||||||
helpText,
|
|
||||||
helpTexts,
|
|
||||||
helpTextWarning,
|
|
||||||
helpLink,
|
|
||||||
pending,
|
|
||||||
errors,
|
|
||||||
warnings,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const InputComponent = getComponent(type);
|
|
||||||
const checkInput = type === inputTypes.CHECK;
|
|
||||||
const hasError = !!errors.length;
|
|
||||||
const hasWarning = !hasError && !!warnings.length;
|
|
||||||
const buttonsArray = React.Children.toArray(buttons);
|
|
||||||
const lastButtonIndex = buttonsArray.length - 1;
|
|
||||||
const hasButton = !!buttonsArray.length;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={containerClassName}>
|
|
||||||
<div className={className}>
|
|
||||||
<div className={styles.inputContainer}>
|
|
||||||
<InputComponent
|
|
||||||
className={inputClassName}
|
|
||||||
helpText={helpText}
|
|
||||||
helpTextWarning={helpTextWarning}
|
|
||||||
hasError={hasError}
|
|
||||||
hasWarning={hasWarning}
|
|
||||||
hasButton={hasButton}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
unit &&
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
type === inputTypes.NUMBER ?
|
|
||||||
styles.inputUnitNumber :
|
|
||||||
styles.inputUnit
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{unit}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
buttonsArray.map((button, index) => {
|
|
||||||
return React.cloneElement(
|
|
||||||
button,
|
|
||||||
{
|
|
||||||
isLastButton: index === lastButtonIndex
|
|
||||||
}
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
{/* <div className={styles.pendingChangesContainer}>
|
|
||||||
{
|
|
||||||
pending &&
|
|
||||||
<Icon
|
|
||||||
name={icons.UNSAVED_SETTING}
|
|
||||||
className={styles.pendingChangesIcon}
|
|
||||||
title="Change has not been saved yet"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
!checkInput && helpText &&
|
|
||||||
<FormInputHelpText
|
|
||||||
text={helpText}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!checkInput && helpTexts &&
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
helpTexts.map((text, index) => {
|
|
||||||
return (
|
|
||||||
<FormInputHelpText
|
|
||||||
key={index}
|
|
||||||
text={text}
|
|
||||||
isCheckInput={checkInput}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
(!checkInput || helpText) && helpTextWarning &&
|
|
||||||
<FormInputHelpText
|
|
||||||
text={helpTextWarning}
|
|
||||||
isWarning={true}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
helpLink &&
|
|
||||||
<Link
|
|
||||||
to={helpLink}
|
|
||||||
>
|
|
||||||
{translate('MoreInfo')}
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
errors.map((error, index) => {
|
|
||||||
return (
|
|
||||||
<FormInputHelpText
|
|
||||||
key={index}
|
|
||||||
text={error.message}
|
|
||||||
link={error.link}
|
|
||||||
tooltip={error.detailedMessage}
|
|
||||||
isError={true}
|
|
||||||
isCheckInput={checkInput}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
warnings.map((warning, index) => {
|
|
||||||
return (
|
|
||||||
<FormInputHelpText
|
|
||||||
key={index}
|
|
||||||
text={warning.message}
|
|
||||||
link={warning.link}
|
|
||||||
tooltip={warning.detailedMessage}
|
|
||||||
isWarning={true}
|
|
||||||
isCheckInput={checkInput}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
FormInputGroup.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
containerClassName: PropTypes.string.isRequired,
|
|
||||||
inputClassName: PropTypes.string,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.any,
|
|
||||||
values: PropTypes.arrayOf(PropTypes.any),
|
|
||||||
isDisabled: PropTypes.bool,
|
|
||||||
type: PropTypes.string.isRequired,
|
|
||||||
kind: PropTypes.oneOf(kinds.all),
|
|
||||||
min: PropTypes.number,
|
|
||||||
max: PropTypes.number,
|
|
||||||
unit: PropTypes.string,
|
|
||||||
buttons: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
|
|
||||||
helpText: PropTypes.string,
|
|
||||||
helpTexts: PropTypes.arrayOf(PropTypes.string),
|
|
||||||
helpTextWarning: PropTypes.string,
|
|
||||||
helpLink: PropTypes.string,
|
|
||||||
autoFocus: PropTypes.bool,
|
|
||||||
includeNoChange: PropTypes.bool,
|
|
||||||
includeNoChangeDisabled: PropTypes.bool,
|
|
||||||
selectedValueOptions: PropTypes.object,
|
|
||||||
indexerFlags: PropTypes.number,
|
|
||||||
pending: PropTypes.bool,
|
|
||||||
errors: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
warnings: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
onChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
FormInputGroup.defaultProps = {
|
|
||||||
className: styles.inputGroup,
|
|
||||||
containerClassName: styles.inputGroupContainer,
|
|
||||||
type: inputTypes.TEXT,
|
|
||||||
buttons: [],
|
|
||||||
helpTexts: [],
|
|
||||||
errors: [],
|
|
||||||
warnings: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FormInputGroup;
|
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import { inputTypes } from 'Helpers/Props';
|
||||||
|
import { InputType } from 'Helpers/Props/inputTypes';
|
||||||
|
import { Kind } from 'Helpers/Props/kinds';
|
||||||
|
import { ValidationError, ValidationWarning } from 'typings/pending';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import AutoCompleteInput from './AutoCompleteInput';
|
||||||
|
import CaptchaInput from './CaptchaInput';
|
||||||
|
import CheckInput from './CheckInput';
|
||||||
|
import { FormInputButtonProps } from './FormInputButton';
|
||||||
|
import FormInputHelpText from './FormInputHelpText';
|
||||||
|
import NumberInput from './NumberInput';
|
||||||
|
import OAuthInput from './OAuthInput';
|
||||||
|
import PasswordInput from './PasswordInput';
|
||||||
|
import PathInput from './PathInput';
|
||||||
|
import DownloadClientSelectInput from './Select/DownloadClientSelectInput';
|
||||||
|
import EnhancedSelectInput from './Select/EnhancedSelectInput';
|
||||||
|
import IndexerFlagsSelectInput from './Select/IndexerFlagsSelectInput';
|
||||||
|
import IndexerSelectInput from './Select/IndexerSelectInput';
|
||||||
|
import MonitorEpisodesSelectInput from './Select/MonitorEpisodesSelectInput';
|
||||||
|
import MonitorNewItemsSelectInput from './Select/MonitorNewItemsSelectInput';
|
||||||
|
import ProviderDataSelectInput from './Select/ProviderOptionSelectInput';
|
||||||
|
import QualityProfileSelectInput from './Select/QualityProfileSelectInput';
|
||||||
|
import RootFolderSelectInput from './Select/RootFolderSelectInput';
|
||||||
|
import SeriesTypeSelectInput from './Select/SeriesTypeSelectInput';
|
||||||
|
import UMaskInput from './Select/UMaskInput';
|
||||||
|
import DeviceInput from './Tag/DeviceInput';
|
||||||
|
import SeriesTagInput from './Tag/SeriesTagInput';
|
||||||
|
import TagSelectInput from './Tag/TagSelectInput';
|
||||||
|
import TextTagInput from './Tag/TextTagInput';
|
||||||
|
import TextArea from './TextArea';
|
||||||
|
import TextInput from './TextInput';
|
||||||
|
import styles from './FormInputGroup.css';
|
||||||
|
|
||||||
|
function getComponent(type: InputType) {
|
||||||
|
switch (type) {
|
||||||
|
case inputTypes.AUTO_COMPLETE:
|
||||||
|
return AutoCompleteInput;
|
||||||
|
|
||||||
|
case inputTypes.CAPTCHA:
|
||||||
|
return CaptchaInput;
|
||||||
|
|
||||||
|
case inputTypes.CHECK:
|
||||||
|
return CheckInput;
|
||||||
|
|
||||||
|
case inputTypes.DEVICE:
|
||||||
|
return DeviceInput;
|
||||||
|
|
||||||
|
case inputTypes.MONITOR_EPISODES_SELECT:
|
||||||
|
return MonitorEpisodesSelectInput;
|
||||||
|
|
||||||
|
case inputTypes.MONITOR_NEW_ITEMS_SELECT:
|
||||||
|
return MonitorNewItemsSelectInput;
|
||||||
|
|
||||||
|
case inputTypes.NUMBER:
|
||||||
|
return NumberInput;
|
||||||
|
|
||||||
|
case inputTypes.OAUTH:
|
||||||
|
return OAuthInput;
|
||||||
|
|
||||||
|
case inputTypes.PASSWORD:
|
||||||
|
return PasswordInput;
|
||||||
|
|
||||||
|
case inputTypes.PATH:
|
||||||
|
return PathInput;
|
||||||
|
|
||||||
|
case inputTypes.QUALITY_PROFILE_SELECT:
|
||||||
|
return QualityProfileSelectInput;
|
||||||
|
|
||||||
|
case inputTypes.INDEXER_SELECT:
|
||||||
|
return IndexerSelectInput;
|
||||||
|
|
||||||
|
case inputTypes.INDEXER_FLAGS_SELECT:
|
||||||
|
return IndexerFlagsSelectInput;
|
||||||
|
|
||||||
|
case inputTypes.DOWNLOAD_CLIENT_SELECT:
|
||||||
|
return DownloadClientSelectInput;
|
||||||
|
|
||||||
|
case inputTypes.ROOT_FOLDER_SELECT:
|
||||||
|
return RootFolderSelectInput;
|
||||||
|
|
||||||
|
case inputTypes.SELECT:
|
||||||
|
return EnhancedSelectInput;
|
||||||
|
|
||||||
|
case inputTypes.DYNAMIC_SELECT:
|
||||||
|
return ProviderDataSelectInput;
|
||||||
|
|
||||||
|
case inputTypes.TAG:
|
||||||
|
case inputTypes.SERIES_TAG:
|
||||||
|
return SeriesTagInput;
|
||||||
|
|
||||||
|
case inputTypes.SERIES_TYPE_SELECT:
|
||||||
|
return SeriesTypeSelectInput;
|
||||||
|
|
||||||
|
case inputTypes.TEXT_AREA:
|
||||||
|
return TextArea;
|
||||||
|
|
||||||
|
case inputTypes.TEXT_TAG:
|
||||||
|
return TextTagInput;
|
||||||
|
|
||||||
|
case inputTypes.TAG_SELECT:
|
||||||
|
return TagSelectInput;
|
||||||
|
|
||||||
|
case inputTypes.UMASK:
|
||||||
|
return UMaskInput;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return TextInput;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Remove once all parent components are updated to TSX and we can refactor to a consistent type
|
||||||
|
interface ValidationMessage {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormInputGroupProps<T> {
|
||||||
|
className?: string;
|
||||||
|
containerClassName?: string;
|
||||||
|
inputClassName?: string;
|
||||||
|
name: string;
|
||||||
|
value?: unknown;
|
||||||
|
values?: unknown[];
|
||||||
|
isDisabled?: boolean;
|
||||||
|
type?: InputType;
|
||||||
|
kind?: Kind;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
unit?: string;
|
||||||
|
buttons?: ReactNode | ReactNode[];
|
||||||
|
helpText?: string;
|
||||||
|
helpTexts?: string[];
|
||||||
|
helpTextWarning?: string;
|
||||||
|
helpLink?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
autoFocus?: boolean;
|
||||||
|
includeNoChange?: boolean;
|
||||||
|
includeNoChangeDisabled?: boolean;
|
||||||
|
selectedValueOptions?: object;
|
||||||
|
indexerFlags?: number;
|
||||||
|
pending?: boolean;
|
||||||
|
canEdit?: boolean;
|
||||||
|
includeAny?: boolean;
|
||||||
|
delimiters?: string[];
|
||||||
|
errors?: (ValidationMessage | ValidationError)[];
|
||||||
|
warnings?: (ValidationMessage | ValidationWarning)[];
|
||||||
|
onChange: (args: T) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormInputGroup<T>(props: FormInputGroupProps<T>) {
|
||||||
|
const {
|
||||||
|
className = styles.inputGroup,
|
||||||
|
containerClassName = styles.inputGroupContainer,
|
||||||
|
inputClassName,
|
||||||
|
type = 'text',
|
||||||
|
unit,
|
||||||
|
buttons = [],
|
||||||
|
helpText,
|
||||||
|
helpTexts = [],
|
||||||
|
helpTextWarning,
|
||||||
|
helpLink,
|
||||||
|
pending,
|
||||||
|
errors = [],
|
||||||
|
warnings = [],
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const InputComponent = getComponent(type);
|
||||||
|
const checkInput = type === inputTypes.CHECK;
|
||||||
|
const hasError = !!errors.length;
|
||||||
|
const hasWarning = !hasError && !!warnings.length;
|
||||||
|
const buttonsArray = React.Children.toArray(buttons);
|
||||||
|
const lastButtonIndex = buttonsArray.length - 1;
|
||||||
|
const hasButton = !!buttonsArray.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={containerClassName}>
|
||||||
|
<div className={className}>
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
{/* @ts-expect-error - need to pass through all the expected options */}
|
||||||
|
<InputComponent
|
||||||
|
className={inputClassName}
|
||||||
|
helpText={helpText}
|
||||||
|
helpTextWarning={helpTextWarning}
|
||||||
|
hasError={hasError}
|
||||||
|
hasWarning={hasWarning}
|
||||||
|
hasButton={hasButton}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{unit && (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
type === inputTypes.NUMBER
|
||||||
|
? styles.inputUnitNumber
|
||||||
|
: styles.inputUnit
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{unit}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{buttonsArray.map((button, index) => {
|
||||||
|
if (!React.isValidElement<FormInputButtonProps>(button)) {
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
return React.cloneElement(button, {
|
||||||
|
isLastButton: index === lastButtonIndex,
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* <div className={styles.pendingChangesContainer}>
|
||||||
|
{
|
||||||
|
pending &&
|
||||||
|
<Icon
|
||||||
|
name={icons.UNSAVED_SETTING}
|
||||||
|
className={styles.pendingChangesIcon}
|
||||||
|
title="Change has not been saved yet"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!checkInput && helpText ? <FormInputHelpText text={helpText} /> : null}
|
||||||
|
|
||||||
|
{!checkInput && helpTexts ? (
|
||||||
|
<div>
|
||||||
|
{helpTexts.map((text, index) => {
|
||||||
|
return (
|
||||||
|
<FormInputHelpText
|
||||||
|
key={index}
|
||||||
|
text={text}
|
||||||
|
isCheckInput={checkInput}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{(!checkInput || helpText) && helpTextWarning ? (
|
||||||
|
<FormInputHelpText text={helpTextWarning} isWarning={true} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{helpLink ? <Link to={helpLink}>{translate('MoreInfo')}</Link> : null}
|
||||||
|
|
||||||
|
{errors.map((error, index) => {
|
||||||
|
return 'message' in error ? (
|
||||||
|
<FormInputHelpText
|
||||||
|
key={index}
|
||||||
|
text={error.message}
|
||||||
|
isError={true}
|
||||||
|
isCheckInput={checkInput}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormInputHelpText
|
||||||
|
key={index}
|
||||||
|
text={error.errorMessage}
|
||||||
|
link={error.infoLink}
|
||||||
|
tooltip={error.detailedDescription}
|
||||||
|
isError={true}
|
||||||
|
isCheckInput={checkInput}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{warnings.map((warning, index) => {
|
||||||
|
return 'message' in warning ? (
|
||||||
|
<FormInputHelpText
|
||||||
|
key={index}
|
||||||
|
text={warning.message}
|
||||||
|
isWarning={true}
|
||||||
|
isCheckInput={checkInput}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormInputHelpText
|
||||||
|
key={index}
|
||||||
|
text={warning.errorMessage}
|
||||||
|
link={warning.infoLink}
|
||||||
|
tooltip={warning.detailedDescription}
|
||||||
|
isWarning={true}
|
||||||
|
isCheckInput={checkInput}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FormInputGroup;
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import styles from './FormInputHelpText.css';
|
|
||||||
|
|
||||||
function FormInputHelpText(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
text,
|
|
||||||
link,
|
|
||||||
tooltip,
|
|
||||||
isError,
|
|
||||||
isWarning,
|
|
||||||
isCheckInput
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames(
|
|
||||||
className,
|
|
||||||
isError && styles.isError,
|
|
||||||
isWarning && styles.isWarning,
|
|
||||||
isCheckInput && styles.isCheckInput
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
|
|
||||||
{
|
|
||||||
link ?
|
|
||||||
<Link
|
|
||||||
className={styles.link}
|
|
||||||
to={link}
|
|
||||||
title={tooltip}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name={icons.EXTERNAL_LINK}
|
|
||||||
/>
|
|
||||||
</Link> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!link && tooltip ?
|
|
||||||
<Icon
|
|
||||||
containerClassName={styles.details}
|
|
||||||
name={icons.INFO}
|
|
||||||
title={tooltip}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
FormInputHelpText.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
text: PropTypes.string.isRequired,
|
|
||||||
link: PropTypes.string,
|
|
||||||
tooltip: PropTypes.string,
|
|
||||||
isError: PropTypes.bool,
|
|
||||||
isWarning: PropTypes.bool,
|
|
||||||
isCheckInput: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
FormInputHelpText.defaultProps = {
|
|
||||||
className: styles.helpText,
|
|
||||||
isError: false,
|
|
||||||
isWarning: false,
|
|
||||||
isCheckInput: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FormInputHelpText;
|
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import styles from './FormInputHelpText.css';
|
||||||
|
|
||||||
|
interface FormInputHelpTextProps {
|
||||||
|
className?: string;
|
||||||
|
text: string;
|
||||||
|
link?: string;
|
||||||
|
tooltip?: string;
|
||||||
|
isError?: boolean;
|
||||||
|
isWarning?: boolean;
|
||||||
|
isCheckInput?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormInputHelpText({
|
||||||
|
className = styles.helpText,
|
||||||
|
text,
|
||||||
|
link,
|
||||||
|
tooltip,
|
||||||
|
isError = false,
|
||||||
|
isWarning = false,
|
||||||
|
isCheckInput = false,
|
||||||
|
}: FormInputHelpTextProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
className,
|
||||||
|
isError && styles.isError,
|
||||||
|
isWarning && styles.isWarning,
|
||||||
|
isCheckInput && styles.isCheckInput
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
|
||||||
|
{link ? (
|
||||||
|
<Link className={styles.link} to={link} title={tooltip}>
|
||||||
|
<Icon name={icons.EXTERNAL_LINK} />
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!link && tooltip ? (
|
||||||
|
<Icon
|
||||||
|
containerClassName={styles.details}
|
||||||
|
name={icons.INFO}
|
||||||
|
title={tooltip}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FormInputHelpText;
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { sizes } from 'Helpers/Props';
|
|
||||||
import styles from './FormLabel.css';
|
|
||||||
|
|
||||||
function FormLabel(props) {
|
|
||||||
const {
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
errorClassName,
|
|
||||||
size,
|
|
||||||
name,
|
|
||||||
hasError,
|
|
||||||
isAdvanced,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<label
|
|
||||||
{...otherProps}
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
styles[size],
|
|
||||||
hasError && errorClassName,
|
|
||||||
isAdvanced && styles.isAdvanced
|
|
||||||
)}
|
|
||||||
htmlFor={name}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
FormLabel.propTypes = {
|
|
||||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.string]).isRequired,
|
|
||||||
className: PropTypes.string,
|
|
||||||
errorClassName: PropTypes.string,
|
|
||||||
size: PropTypes.oneOf(sizes.all),
|
|
||||||
name: PropTypes.string,
|
|
||||||
hasError: PropTypes.bool,
|
|
||||||
isAdvanced: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
FormLabel.defaultProps = {
|
|
||||||
className: styles.label,
|
|
||||||
errorClassName: styles.hasError,
|
|
||||||
isAdvanced: false,
|
|
||||||
size: sizes.LARGE
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FormLabel;
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { Size } from 'Helpers/Props/sizes';
|
||||||
|
import styles from './FormLabel.css';
|
||||||
|
|
||||||
|
interface FormLabelProps {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
errorClassName?: string;
|
||||||
|
size?: Extract<Size, keyof typeof styles>;
|
||||||
|
name?: string;
|
||||||
|
hasError?: boolean;
|
||||||
|
isAdvanced?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormLabel(props: FormLabelProps) {
|
||||||
|
const {
|
||||||
|
children,
|
||||||
|
className = styles.label,
|
||||||
|
errorClassName = styles.hasError,
|
||||||
|
size = 'large',
|
||||||
|
name,
|
||||||
|
hasError,
|
||||||
|
isAdvanced = false,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
className={classNames(
|
||||||
|
className,
|
||||||
|
styles[size],
|
||||||
|
hasError && errorClassName,
|
||||||
|
isAdvanced && styles.isAdvanced
|
||||||
|
)}
|
||||||
|
htmlFor={name}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FormLabel;
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import EnhancedSelectInputOption from './EnhancedSelectInputOption';
|
|
||||||
import styles from './HintedSelectInputOption.css';
|
|
||||||
|
|
||||||
function HintedSelectInputOption(props) {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
value,
|
|
||||||
hint,
|
|
||||||
depth,
|
|
||||||
isSelected,
|
|
||||||
isDisabled,
|
|
||||||
isMultiSelect,
|
|
||||||
isMobile,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EnhancedSelectInputOption
|
|
||||||
id={id}
|
|
||||||
depth={depth}
|
|
||||||
isSelected={isSelected}
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
isHidden={isDisabled}
|
|
||||||
isMultiSelect={isMultiSelect}
|
|
||||||
isMobile={isMobile}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<div className={classNames(
|
|
||||||
styles.optionText,
|
|
||||||
isMobile && styles.isMobile
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div>{value}</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
hint != null &&
|
|
||||||
<div className={styles.hintText}>
|
|
||||||
{hint}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</EnhancedSelectInputOption>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
HintedSelectInputOption.propTypes = {
|
|
||||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
|
||||||
value: PropTypes.string.isRequired,
|
|
||||||
hint: PropTypes.node,
|
|
||||||
depth: PropTypes.number,
|
|
||||||
isSelected: PropTypes.bool.isRequired,
|
|
||||||
isDisabled: PropTypes.bool.isRequired,
|
|
||||||
isMultiSelect: PropTypes.bool.isRequired,
|
|
||||||
isMobile: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
HintedSelectInputOption.defaultProps = {
|
|
||||||
isDisabled: false,
|
|
||||||
isHidden: false,
|
|
||||||
isMultiSelect: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HintedSelectInputOption;
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue';
|
|
||||||
import styles from './HintedSelectInputSelectedValue.css';
|
|
||||||
|
|
||||||
function HintedSelectInputSelectedValue(props) {
|
|
||||||
const {
|
|
||||||
value,
|
|
||||||
values,
|
|
||||||
hint,
|
|
||||||
isMultiSelect,
|
|
||||||
includeHint,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const valuesMap = isMultiSelect && _.keyBy(values, 'key');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EnhancedSelectInputSelectedValue
|
|
||||||
className={styles.selectedValue}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<div className={styles.valueText}>
|
|
||||||
{
|
|
||||||
isMultiSelect ?
|
|
||||||
value.map((key, index) => {
|
|
||||||
const v = valuesMap[key];
|
|
||||||
return (
|
|
||||||
<Label key={key}>
|
|
||||||
{v ? v.value : key}
|
|
||||||
</Label>
|
|
||||||
);
|
|
||||||
}) :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isMultiSelect ? null : value
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
hint != null && includeHint ?
|
|
||||||
<div className={styles.hintText}>
|
|
||||||
{hint}
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</EnhancedSelectInputSelectedValue>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
HintedSelectInputSelectedValue.propTypes = {
|
|
||||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number]))]).isRequired,
|
|
||||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
hint: PropTypes.string,
|
|
||||||
isMultiSelect: PropTypes.bool.isRequired,
|
|
||||||
includeHint: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
HintedSelectInputSelectedValue.defaultProps = {
|
|
||||||
isMultiSelect: false,
|
|
||||||
includeHint: true
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HintedSelectInputSelectedValue;
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { fetchIndexers } from 'Store/Actions/settingsActions';
|
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.settings.indexers,
|
|
||||||
(state, { includeAny }) => includeAny,
|
|
||||||
(indexers, includeAny) => {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items
|
|
||||||
} = indexers;
|
|
||||||
|
|
||||||
const values = _.map(items.sort(sortByProp('name')), (indexer) => {
|
|
||||||
return {
|
|
||||||
key: indexer.id,
|
|
||||||
value: indexer.name
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (includeAny) {
|
|
||||||
values.unshift({
|
|
||||||
key: 0,
|
|
||||||
value: `(${translate('Any')})`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
values
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchFetchIndexers: fetchIndexers
|
|
||||||
};
|
|
||||||
|
|
||||||
class IndexerSelectInputConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
if (!this.props.isPopulated) {
|
|
||||||
this.props.dispatchFetchIndexers();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onChange = ({ name, value }) => {
|
|
||||||
this.props.onChange({ name, value: parseInt(value) });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<EnhancedSelectInput
|
|
||||||
{...this.props}
|
|
||||||
onChange={this.onChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IndexerSelectInputConnector.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
|
||||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
includeAny: PropTypes.bool.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
dispatchFetchIndexers: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
IndexerSelectInputConnector.defaultProps = {
|
|
||||||
includeAny: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerSelectInputConnector);
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
.inputContainer {
|
|
||||||
composes: input from '~Components/Form/Input.css';
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
min-height: 35px;
|
|
||||||
height: auto;
|
|
||||||
|
|
||||||
&.isFocused {
|
|
||||||
outline: 0;
|
|
||||||
border-color: var(--inputFocusBorderColor);
|
|
||||||
box-shadow: inset 0 1px 1px var(--inputBoxShadowColor), 0 0 8px var(--inputFocusBoxShadowColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hasError {
|
|
||||||
composes: hasError from '~Components/Form/Input.css';
|
|
||||||
}
|
|
||||||
|
|
||||||
.hasWarning {
|
|
||||||
composes: hasWarning from '~Components/Form/Input.css';
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
// This file is automatically generated.
|
|
||||||
// Please do not change this file!
|
|
||||||
interface CssExports {
|
|
||||||
'hasError': string;
|
|
||||||
'hasWarning': string;
|
|
||||||
'inputContainer': string;
|
|
||||||
'isFocused': string;
|
|
||||||
}
|
|
||||||
export const cssExports: CssExports;
|
|
||||||
export default cssExports;
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import KeyValueListInputItem from './KeyValueListInputItem';
|
|
||||||
import styles from './KeyValueListInput.css';
|
|
||||||
|
|
||||||
class KeyValueListInput extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isFocused: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onItemChange = (index, itemValue) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const newValue = [...value];
|
|
||||||
|
|
||||||
if (index == null) {
|
|
||||||
newValue.push(itemValue);
|
|
||||||
} else {
|
|
||||||
newValue.splice(index, 1, itemValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange({
|
|
||||||
name,
|
|
||||||
value: newValue
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveItem = (index) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const newValue = [...value];
|
|
||||||
newValue.splice(index, 1);
|
|
||||||
|
|
||||||
onChange({
|
|
||||||
name,
|
|
||||||
value: newValue
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onFocus = () => {
|
|
||||||
this.setState({
|
|
||||||
isFocused: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onBlur = () => {
|
|
||||||
this.setState({
|
|
||||||
isFocused: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const newValue = value.reduce((acc, v) => {
|
|
||||||
if (v.key || v.value) {
|
|
||||||
acc.push(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (newValue.length !== value.length) {
|
|
||||||
onChange({
|
|
||||||
name,
|
|
||||||
value: newValue
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
value,
|
|
||||||
keyPlaceholder,
|
|
||||||
valuePlaceholder,
|
|
||||||
hasError,
|
|
||||||
hasWarning
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const { isFocused } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames(
|
|
||||||
className,
|
|
||||||
isFocused && styles.isFocused,
|
|
||||||
hasError && styles.hasError,
|
|
||||||
hasWarning && styles.hasWarning
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
[...value, { key: '', value: '' }].map((v, index) => {
|
|
||||||
return (
|
|
||||||
<KeyValueListInputItem
|
|
||||||
key={index}
|
|
||||||
index={index}
|
|
||||||
keyValue={v.key}
|
|
||||||
value={v.value}
|
|
||||||
keyPlaceholder={keyPlaceholder}
|
|
||||||
valuePlaceholder={valuePlaceholder}
|
|
||||||
isNew={index === value.length}
|
|
||||||
onChange={this.onItemChange}
|
|
||||||
onRemove={this.onRemoveItem}
|
|
||||||
onFocus={this.onFocus}
|
|
||||||
onBlur={this.onBlur}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
KeyValueListInput.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
hasError: PropTypes.bool,
|
|
||||||
hasWarning: PropTypes.bool,
|
|
||||||
keyPlaceholder: PropTypes.string,
|
|
||||||
valuePlaceholder: PropTypes.string,
|
|
||||||
onChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
KeyValueListInput.defaultProps = {
|
|
||||||
className: styles.inputContainer,
|
|
||||||
value: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export default KeyValueListInput;
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
.itemContainer {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 3px;
|
|
||||||
border-bottom: 1px solid var(--inputBorderColor);
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.keyInputWrapper {
|
|
||||||
flex: 6 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.valueInputWrapper {
|
|
||||||
flex: 1 0 0;
|
|
||||||
min-width: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonWrapper {
|
|
||||||
flex: 0 0 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.keyInput,
|
|
||||||
.valueInput {
|
|
||||||
width: 100%;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
// This file is automatically generated.
|
|
||||||
// Please do not change this file!
|
|
||||||
interface CssExports {
|
|
||||||
'buttonWrapper': string;
|
|
||||||
'itemContainer': string;
|
|
||||||
'keyInput': string;
|
|
||||||
'keyInputWrapper': string;
|
|
||||||
'valueInput': string;
|
|
||||||
'valueInputWrapper': string;
|
|
||||||
}
|
|
||||||
export const cssExports: CssExports;
|
|
||||||
export default cssExports;
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import TextInput from './TextInput';
|
|
||||||
import styles from './KeyValueListInputItem.css';
|
|
||||||
|
|
||||||
class KeyValueListInputItem extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onKeyChange = ({ value: keyValue }) => {
|
|
||||||
const {
|
|
||||||
index,
|
|
||||||
value,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onChange(index, { key: keyValue, value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onValueChange = ({ value }) => {
|
|
||||||
// TODO: Validate here or validate at a lower level component
|
|
||||||
|
|
||||||
const {
|
|
||||||
index,
|
|
||||||
keyValue,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onChange(index, { key: keyValue, value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemovePress = () => {
|
|
||||||
const {
|
|
||||||
index,
|
|
||||||
onRemove
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onRemove(index);
|
|
||||||
};
|
|
||||||
|
|
||||||
onFocus = () => {
|
|
||||||
this.props.onFocus();
|
|
||||||
};
|
|
||||||
|
|
||||||
onBlur = () => {
|
|
||||||
this.props.onBlur();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
keyValue,
|
|
||||||
value,
|
|
||||||
keyPlaceholder,
|
|
||||||
valuePlaceholder,
|
|
||||||
isNew
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.itemContainer}>
|
|
||||||
<div className={styles.keyInputWrapper}>
|
|
||||||
<TextInput
|
|
||||||
className={styles.keyInput}
|
|
||||||
name="key"
|
|
||||||
value={keyValue}
|
|
||||||
placeholder={keyPlaceholder}
|
|
||||||
onChange={this.onKeyChange}
|
|
||||||
onFocus={this.onFocus}
|
|
||||||
onBlur={this.onBlur}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.valueInputWrapper}>
|
|
||||||
<TextInput
|
|
||||||
className={styles.valueInput}
|
|
||||||
name="value"
|
|
||||||
value={value}
|
|
||||||
placeholder={valuePlaceholder}
|
|
||||||
onChange={this.onValueChange}
|
|
||||||
onFocus={this.onFocus}
|
|
||||||
onBlur={this.onBlur}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.buttonWrapper}>
|
|
||||||
{
|
|
||||||
isNew ?
|
|
||||||
null :
|
|
||||||
<IconButton
|
|
||||||
name={icons.REMOVE}
|
|
||||||
tabIndex={-1}
|
|
||||||
onPress={this.onRemovePress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
KeyValueListInputItem.propTypes = {
|
|
||||||
index: PropTypes.number,
|
|
||||||
keyValue: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
|
||||||
keyPlaceholder: PropTypes.string.isRequired,
|
|
||||||
valuePlaceholder: PropTypes.string.isRequired,
|
|
||||||
isNew: PropTypes.bool.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
onRemove: PropTypes.func.isRequired,
|
|
||||||
onFocus: PropTypes.func.isRequired,
|
|
||||||
onBlur: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
KeyValueListInputItem.defaultProps = {
|
|
||||||
keyPlaceholder: 'Key',
|
|
||||||
valuePlaceholder: 'Value'
|
|
||||||
};
|
|
||||||
|
|
||||||
export default KeyValueListInputItem;
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state, { values }) => values,
|
|
||||||
( languages ) => {
|
|
||||||
|
|
||||||
const minId = languages.reduce((min, v) => (v.key < 1 ? v.key : min), languages[0].key);
|
|
||||||
|
|
||||||
const values = languages.map(({ key, value }) => {
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
dividerAfter: minId < 1 ? key === minId : false
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
values
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class LanguageSelectInputConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EnhancedSelectInput
|
|
||||||
{...this.props}
|
|
||||||
onChange={this.props.onChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LanguageSelectInputConnector.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.number]).isRequired,
|
|
||||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(LanguageSelectInputConnector);
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import monitorOptions from 'Utilities/Series/monitorOptions';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
|
||||||
|
|
||||||
function MonitorEpisodesSelectInput(props) {
|
|
||||||
const {
|
|
||||||
includeNoChange,
|
|
||||||
includeMixed,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const values = [...monitorOptions];
|
|
||||||
|
|
||||||
if (includeNoChange) {
|
|
||||||
values.unshift({
|
|
||||||
key: 'noChange',
|
|
||||||
get value() {
|
|
||||||
return translate('NoChange');
|
|
||||||
},
|
|
||||||
isDisabled: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeMixed) {
|
|
||||||
values.unshift({
|
|
||||||
key: 'mixed',
|
|
||||||
get value() {
|
|
||||||
return `(${translate('Mixed')})`;
|
|
||||||
},
|
|
||||||
isDisabled: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EnhancedSelectInput
|
|
||||||
values={values}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MonitorEpisodesSelectInput.propTypes = {
|
|
||||||
includeNoChange: PropTypes.bool.isRequired,
|
|
||||||
includeMixed: PropTypes.bool.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
MonitorEpisodesSelectInput.defaultProps = {
|
|
||||||
includeNoChange: false,
|
|
||||||
includeMixed: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MonitorEpisodesSelectInput;
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import monitorNewItemsOptions from 'Utilities/Series/monitorNewItemsOptions';
|
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
|
||||||
|
|
||||||
function MonitorNewItemsSelectInput(props) {
|
|
||||||
const {
|
|
||||||
includeNoChange,
|
|
||||||
includeMixed,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const values = [...monitorNewItemsOptions];
|
|
||||||
|
|
||||||
if (includeNoChange) {
|
|
||||||
values.unshift({
|
|
||||||
key: 'noChange',
|
|
||||||
value: 'No Change',
|
|
||||||
isDisabled: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeMixed) {
|
|
||||||
values.unshift({
|
|
||||||
key: 'mixed',
|
|
||||||
value: '(Mixed)',
|
|
||||||
isDisabled: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EnhancedSelectInput
|
|
||||||
values={values}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MonitorNewItemsSelectInput.propTypes = {
|
|
||||||
includeNoChange: PropTypes.bool.isRequired,
|
|
||||||
includeMixed: PropTypes.bool.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
MonitorNewItemsSelectInput.defaultProps = {
|
|
||||||
includeNoChange: false,
|
|
||||||
includeMixed: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MonitorNewItemsSelectInput;
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import TextInput from './TextInput';
|
|
||||||
|
|
||||||
function parseValue(props, value) {
|
|
||||||
const {
|
|
||||||
isFloat,
|
|
||||||
min,
|
|
||||||
max
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (value == null || value === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let newValue = isFloat ? parseFloat(value) : parseInt(value);
|
|
||||||
|
|
||||||
if (min != null && newValue != null && newValue < min) {
|
|
||||||
newValue = min;
|
|
||||||
} else if (max != null && newValue != null && newValue > max) {
|
|
||||||
newValue = max;
|
|
||||||
}
|
|
||||||
|
|
||||||
return newValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
class NumberInput extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
value: props.value == null ? '' : props.value.toString(),
|
|
||||||
isFocused: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
|
||||||
const { value } = this.props;
|
|
||||||
|
|
||||||
if (!isNaN(value) && value !== prevProps.value && !this.state.isFocused) {
|
|
||||||
this.setState({
|
|
||||||
value: value == null ? '' : value.toString()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onChange = ({ name, value }) => {
|
|
||||||
this.setState({ value });
|
|
||||||
|
|
||||||
this.props.onChange({
|
|
||||||
name,
|
|
||||||
value: parseValue(this.props, value)
|
|
||||||
});
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
onFocus = () => {
|
|
||||||
this.setState({ isFocused: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onBlur = () => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const { value } = this.state;
|
|
||||||
const parsedValue = parseValue(this.props, value);
|
|
||||||
const stringValue = parsedValue == null ? '' : parsedValue.toString();
|
|
||||||
|
|
||||||
if (stringValue === value) {
|
|
||||||
this.setState({ isFocused: false });
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
value: stringValue,
|
|
||||||
isFocused: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange({
|
|
||||||
name,
|
|
||||||
value: parsedValue
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const value = this.state.value;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TextInput
|
|
||||||
{...this.props}
|
|
||||||
type="number"
|
|
||||||
value={value == null ? '' : value}
|
|
||||||
onChange={this.onChange}
|
|
||||||
onBlur={this.onBlur}
|
|
||||||
onFocus={this.onFocus}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberInput.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.number,
|
|
||||||
min: PropTypes.number,
|
|
||||||
max: PropTypes.number,
|
|
||||||
isFloat: PropTypes.bool.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
NumberInput.defaultProps = {
|
|
||||||
value: null,
|
|
||||||
isFloat: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NumberInput;
|
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import TextInput, { TextInputProps } from './TextInput';
|
||||||
|
|
||||||
|
function parseValue(
|
||||||
|
value: string | null | undefined,
|
||||||
|
isFloat: boolean,
|
||||||
|
min: number | undefined,
|
||||||
|
max: number | undefined
|
||||||
|
) {
|
||||||
|
if (value == null || value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newValue = isFloat ? parseFloat(value) : parseInt(value);
|
||||||
|
|
||||||
|
if (min != null && newValue != null && newValue < min) {
|
||||||
|
newValue = min;
|
||||||
|
} else if (max != null && newValue != null && newValue > max) {
|
||||||
|
newValue = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NumberInputProps
|
||||||
|
extends Omit<TextInputProps<number | null>, 'value'> {
|
||||||
|
value?: number | null;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
isFloat?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function NumberInput({
|
||||||
|
name,
|
||||||
|
value: inputValue = null,
|
||||||
|
isFloat = false,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
onChange,
|
||||||
|
...otherProps
|
||||||
|
}: NumberInputProps) {
|
||||||
|
const [value, setValue] = useState(
|
||||||
|
inputValue == null ? '' : inputValue.toString()
|
||||||
|
);
|
||||||
|
const isFocused = useRef(false);
|
||||||
|
const previousValue = usePrevious(inputValue);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
({ name, value: newValue }: InputChanged<string>) => {
|
||||||
|
setValue(newValue);
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: parseValue(newValue, isFloat, min, max),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[isFloat, min, max, onChange, setValue]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFocus = useCallback(() => {
|
||||||
|
isFocused.current = true;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleBlur = useCallback(() => {
|
||||||
|
const parsedValue = parseValue(value, isFloat, min, max);
|
||||||
|
const stringValue = parsedValue == null ? '' : parsedValue.toString();
|
||||||
|
|
||||||
|
if (stringValue !== value) {
|
||||||
|
setValue(stringValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: parsedValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
isFocused.current = false;
|
||||||
|
}, [name, value, isFloat, min, max, onChange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
// @ts-expect-error inputValue may be null
|
||||||
|
!isNaN(inputValue) &&
|
||||||
|
inputValue !== previousValue &&
|
||||||
|
!isFocused.current
|
||||||
|
) {
|
||||||
|
setValue(inputValue == null ? '' : inputValue.toString());
|
||||||
|
}
|
||||||
|
}, [inputValue, previousValue, setValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextInput
|
||||||
|
{...otherProps}
|
||||||
|
name={name}
|
||||||
|
type="number"
|
||||||
|
value={value == null ? '' : value}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
onChange={handleChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NumberInput;
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
|
||||||
import { kinds } from 'Helpers/Props';
|
|
||||||
|
|
||||||
function OAuthInput(props) {
|
|
||||||
const {
|
|
||||||
label,
|
|
||||||
authorizing,
|
|
||||||
error,
|
|
||||||
onPress
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<SpinnerErrorButton
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
isSpinning={authorizing}
|
|
||||||
error={error}
|
|
||||||
onPress={onPress}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</SpinnerErrorButton>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
OAuthInput.propTypes = {
|
|
||||||
label: PropTypes.string.isRequired,
|
|
||||||
authorizing: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
onPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
OAuthInput.defaultProps = {
|
|
||||||
label: 'Start OAuth'
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OAuthInput;
|
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import React, { useCallback, useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import { resetOAuth, startOAuth } from 'Store/Actions/oAuthActions';
|
||||||
|
import { InputOnChange } from 'typings/inputs';
|
||||||
|
|
||||||
|
interface OAuthInputProps {
|
||||||
|
label?: string;
|
||||||
|
name: string;
|
||||||
|
provider: string;
|
||||||
|
providerData: object;
|
||||||
|
section: string;
|
||||||
|
onChange: InputOnChange<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function OAuthInput({
|
||||||
|
label = 'Start OAuth',
|
||||||
|
name,
|
||||||
|
provider,
|
||||||
|
providerData,
|
||||||
|
section,
|
||||||
|
onChange,
|
||||||
|
}: OAuthInputProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { authorizing, error, result } = useSelector(
|
||||||
|
(state: AppState) => state.oAuth
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePress = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
startOAuth({
|
||||||
|
name,
|
||||||
|
provider,
|
||||||
|
providerData,
|
||||||
|
section,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [name, provider, providerData, section, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(result).forEach((key) => {
|
||||||
|
onChange({ name: key, value: result[key] });
|
||||||
|
});
|
||||||
|
}, [result, onChange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
dispatch(resetOAuth());
|
||||||
|
};
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<SpinnerErrorButton
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
isSpinning={authorizing}
|
||||||
|
error={error}
|
||||||
|
onPress={handlePress}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</SpinnerErrorButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OAuthInput;
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { resetOAuth, startOAuth } from 'Store/Actions/oAuthActions';
|
|
||||||
import OAuthInput from './OAuthInput';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.oAuth,
|
|
||||||
(oAuth) => {
|
|
||||||
return oAuth;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
startOAuth,
|
|
||||||
resetOAuth
|
|
||||||
};
|
|
||||||
|
|
||||||
class OAuthInputConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
result,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!result || result === prevProps.result) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.keys(result).forEach((key) => {
|
|
||||||
onChange({ name: key, value: result[key] });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount = () => {
|
|
||||||
this.props.resetOAuth();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onPress = () => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
provider,
|
|
||||||
providerData,
|
|
||||||
section
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
this.props.startOAuth({
|
|
||||||
name,
|
|
||||||
provider,
|
|
||||||
providerData,
|
|
||||||
section
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<OAuthInput
|
|
||||||
{...this.props}
|
|
||||||
onPress={this.onPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
OAuthInputConnector.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
result: PropTypes.object,
|
|
||||||
provider: PropTypes.string.isRequired,
|
|
||||||
providerData: PropTypes.object.isRequired,
|
|
||||||
section: PropTypes.string.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
startOAuth: PropTypes.func.isRequired,
|
|
||||||
resetOAuth: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(OAuthInputConnector);
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import TextInput from './TextInput';
|
|
||||||
|
|
||||||
// Prevent a user from copying (or cutting) the password from the input
|
|
||||||
function onCopy(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.nativeEvent.stopImmediatePropagation();
|
|
||||||
}
|
|
||||||
|
|
||||||
function PasswordInput(props) {
|
|
||||||
return (
|
|
||||||
<TextInput
|
|
||||||
{...props}
|
|
||||||
type="password"
|
|
||||||
onCopy={onCopy}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
PasswordInput.propTypes = {
|
|
||||||
...TextInput.props
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PasswordInput;
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import React, { SyntheticEvent } from 'react';
|
||||||
|
import TextInput, { TextInputProps } from './TextInput';
|
||||||
|
|
||||||
|
// Prevent a user from copying (or cutting) the password from the input
|
||||||
|
function onCopy(e: SyntheticEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.nativeEvent.stopImmediatePropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
function PasswordInput(props: TextInputProps<string>) {
|
||||||
|
return <TextInput {...props} type="password" onCopy={onCopy} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PasswordInput;
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import AutoSuggestInput from './AutoSuggestInput';
|
|
||||||
import FormInputButton from './FormInputButton';
|
|
||||||
import styles from './PathInput.css';
|
|
||||||
|
|
||||||
class PathInput extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._node = document.getElementById('portal-root');
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
value: props.value,
|
|
||||||
isFileBrowserModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const { value } = this.props;
|
|
||||||
|
|
||||||
if (prevProps.value !== value) {
|
|
||||||
this.setState({ value });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
getSuggestionValue({ path }) {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSuggestion({ path }, { query }) {
|
|
||||||
const lastSeparatorIndex = query.lastIndexOf('\\') || query.lastIndexOf('/');
|
|
||||||
|
|
||||||
if (lastSeparatorIndex === -1) {
|
|
||||||
return (
|
|
||||||
<span>{path}</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
<span className={styles.pathMatch}>
|
|
||||||
{path.substr(0, lastSeparatorIndex)}
|
|
||||||
</span>
|
|
||||||
{path.substr(lastSeparatorIndex)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onInputChange = ({ value }) => {
|
|
||||||
this.setState({ value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onInputKeyDown = (event) => {
|
|
||||||
if (event.key === 'Tab') {
|
|
||||||
event.preventDefault();
|
|
||||||
const path = this.props.paths[0];
|
|
||||||
|
|
||||||
if (path) {
|
|
||||||
this.props.onChange({
|
|
||||||
name: this.props.name,
|
|
||||||
value: path.path
|
|
||||||
});
|
|
||||||
|
|
||||||
if (path.type !== 'file') {
|
|
||||||
this.props.onFetchPaths(path.path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onInputBlur = () => {
|
|
||||||
this.props.onChange({
|
|
||||||
name: this.props.name,
|
|
||||||
value: this.state.value
|
|
||||||
});
|
|
||||||
|
|
||||||
this.props.onClearPaths();
|
|
||||||
};
|
|
||||||
|
|
||||||
onSuggestionsFetchRequested = ({ value }) => {
|
|
||||||
this.props.onFetchPaths(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
onSuggestionsClearRequested = () => {
|
|
||||||
// Required because props aren't always rendered, but no-op
|
|
||||||
// because we don't want to reset the paths after a path is selected.
|
|
||||||
};
|
|
||||||
|
|
||||||
onSuggestionSelected = (event, { suggestionValue }) => {
|
|
||||||
this.props.onFetchPaths(suggestionValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
onFileBrowserOpenPress = () => {
|
|
||||||
this.setState({ isFileBrowserModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onFileBrowserModalClose = () => {
|
|
||||||
this.setState({ isFileBrowserModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
name,
|
|
||||||
paths,
|
|
||||||
includeFiles,
|
|
||||||
hasFileBrowser,
|
|
||||||
onChange,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
value,
|
|
||||||
isFileBrowserModalOpen
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
<AutoSuggestInput
|
|
||||||
{...otherProps}
|
|
||||||
className={hasFileBrowser ? styles.hasFileBrowser : undefined}
|
|
||||||
name={name}
|
|
||||||
value={value}
|
|
||||||
suggestions={paths}
|
|
||||||
getSuggestionValue={this.getSuggestionValue}
|
|
||||||
renderSuggestion={this.renderSuggestion}
|
|
||||||
onInputKeyDown={this.onInputKeyDown}
|
|
||||||
onInputBlur={this.onInputBlur}
|
|
||||||
onSuggestionSelected={this.onSuggestionSelected}
|
|
||||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
|
||||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
|
||||||
onChange={this.onInputChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
hasFileBrowser &&
|
|
||||||
<div>
|
|
||||||
<FormInputButton
|
|
||||||
className={styles.fileBrowserButton}
|
|
||||||
onPress={this.onFileBrowserOpenPress}
|
|
||||||
>
|
|
||||||
<Icon name={icons.FOLDER_OPEN} />
|
|
||||||
</FormInputButton>
|
|
||||||
|
|
||||||
<FileBrowserModal
|
|
||||||
isOpen={isFileBrowserModalOpen}
|
|
||||||
name={name}
|
|
||||||
value={value}
|
|
||||||
includeFiles={includeFiles}
|
|
||||||
onChange={onChange}
|
|
||||||
onModalClose={this.onFileBrowserModalClose}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PathInput.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.string,
|
|
||||||
paths: PropTypes.array.isRequired,
|
|
||||||
includeFiles: PropTypes.bool.isRequired,
|
|
||||||
hasFileBrowser: PropTypes.bool,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
onFetchPaths: PropTypes.func.isRequired,
|
|
||||||
onClearPaths: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
PathInput.defaultProps = {
|
|
||||||
className: styles.inputWrapper,
|
|
||||||
value: '',
|
|
||||||
hasFileBrowser: true
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PathInput;
|
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
import React, {
|
||||||
|
KeyboardEvent,
|
||||||
|
SyntheticEvent,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import {
|
||||||
|
ChangeEvent,
|
||||||
|
SuggestionsFetchRequestedParams,
|
||||||
|
} from 'react-autosuggest';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import { Path } from 'App/State/PathsAppState';
|
||||||
|
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import { clearPaths, fetchPaths } from 'Store/Actions/pathActions';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import AutoSuggestInput from './AutoSuggestInput';
|
||||||
|
import FormInputButton from './FormInputButton';
|
||||||
|
import styles from './PathInput.css';
|
||||||
|
|
||||||
|
interface PathInputProps {
|
||||||
|
className?: string;
|
||||||
|
name: string;
|
||||||
|
value?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
includeFiles: boolean;
|
||||||
|
hasFileBrowser?: boolean;
|
||||||
|
onChange: (change: InputChanged<string>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PathInputInternalProps extends PathInputProps {
|
||||||
|
paths: Path[];
|
||||||
|
onFetchPaths: (path: string) => void;
|
||||||
|
onClearPaths: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSuggestionsClearRequested() {
|
||||||
|
// Required because props aren't always rendered, but no-op
|
||||||
|
// because we don't want to reset the paths after a path is selected.
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPathsSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.paths,
|
||||||
|
(paths) => {
|
||||||
|
const { currentPath, directories, files } = paths;
|
||||||
|
|
||||||
|
const filteredPaths = [...directories, ...files].filter(({ path }) => {
|
||||||
|
return path.toLowerCase().startsWith(currentPath.toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
return filteredPaths;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PathInput(props: PathInputProps) {
|
||||||
|
const { includeFiles } = props;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const paths = useSelector(createPathsSelector());
|
||||||
|
|
||||||
|
const handleFetchPaths = useCallback(
|
||||||
|
(path: string) => {
|
||||||
|
dispatch(fetchPaths({ path, includeFiles }));
|
||||||
|
},
|
||||||
|
[includeFiles, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClearPaths = useCallback(() => {
|
||||||
|
dispatch(clearPaths);
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PathInputInternal
|
||||||
|
{...props}
|
||||||
|
paths={paths}
|
||||||
|
onFetchPaths={handleFetchPaths}
|
||||||
|
onClearPaths={handleClearPaths}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PathInput;
|
||||||
|
|
||||||
|
export function PathInputInternal(props: PathInputInternalProps) {
|
||||||
|
const {
|
||||||
|
className = styles.inputWrapper,
|
||||||
|
name,
|
||||||
|
value: inputValue = '',
|
||||||
|
paths,
|
||||||
|
includeFiles,
|
||||||
|
hasFileBrowser = true,
|
||||||
|
onChange,
|
||||||
|
onFetchPaths,
|
||||||
|
onClearPaths,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [value, setValue] = useState(inputValue);
|
||||||
|
const [isFileBrowserModalOpen, setIsFileBrowserModalOpen] = useState(false);
|
||||||
|
const previousInputValue = usePrevious(inputValue);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const handleFetchPaths = useCallback(
|
||||||
|
(path: string) => {
|
||||||
|
dispatch(fetchPaths({ path, includeFiles }));
|
||||||
|
},
|
||||||
|
[includeFiles, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleInputChange = useCallback(
|
||||||
|
(_event: SyntheticEvent, { newValue }: ChangeEvent) => {
|
||||||
|
setValue(newValue);
|
||||||
|
},
|
||||||
|
[setValue]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleInputKeyDown = useCallback(
|
||||||
|
(event: KeyboardEvent<HTMLElement>) => {
|
||||||
|
if (event.key === 'Tab') {
|
||||||
|
event.preventDefault();
|
||||||
|
const path = paths[0];
|
||||||
|
|
||||||
|
if (path) {
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: path.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (path.type !== 'file') {
|
||||||
|
handleFetchPaths(path.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[name, paths, handleFetchPaths, onChange]
|
||||||
|
);
|
||||||
|
const handleInputBlur = useCallback(() => {
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
|
||||||
|
onClearPaths();
|
||||||
|
}, [name, value, onClearPaths, onChange]);
|
||||||
|
|
||||||
|
const handleSuggestionSelected = useCallback(
|
||||||
|
(_event: SyntheticEvent, { suggestion }: { suggestion: Path }) => {
|
||||||
|
handleFetchPaths(suggestion.path);
|
||||||
|
},
|
||||||
|
[handleFetchPaths]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSuggestionsFetchRequested = useCallback(
|
||||||
|
({ value: newValue }: SuggestionsFetchRequestedParams) => {
|
||||||
|
handleFetchPaths(newValue);
|
||||||
|
},
|
||||||
|
[handleFetchPaths]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFileBrowserOpenPress = useCallback(() => {
|
||||||
|
setIsFileBrowserModalOpen(true);
|
||||||
|
}, [setIsFileBrowserModalOpen]);
|
||||||
|
|
||||||
|
const handleFileBrowserModalClose = useCallback(() => {
|
||||||
|
setIsFileBrowserModalOpen(false);
|
||||||
|
}, [setIsFileBrowserModalOpen]);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(change: InputChanged<Path>) => {
|
||||||
|
onChange({ name, value: change.value.path });
|
||||||
|
},
|
||||||
|
[name, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getSuggestionValue = useCallback(({ path }: Path) => path, []);
|
||||||
|
|
||||||
|
const renderSuggestion = useCallback(
|
||||||
|
({ path }: Path, { query }: { query: string }) => {
|
||||||
|
const lastSeparatorIndex =
|
||||||
|
query.lastIndexOf('\\') || query.lastIndexOf('/');
|
||||||
|
|
||||||
|
if (lastSeparatorIndex === -1) {
|
||||||
|
return <span>{path}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<span className={styles.pathMatch}>
|
||||||
|
{path.substring(0, lastSeparatorIndex)}
|
||||||
|
</span>
|
||||||
|
{path.substring(lastSeparatorIndex)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputValue !== previousInputValue) {
|
||||||
|
setValue(inputValue);
|
||||||
|
}
|
||||||
|
}, [inputValue, previousInputValue, setValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<AutoSuggestInput
|
||||||
|
{...otherProps}
|
||||||
|
className={hasFileBrowser ? styles.hasFileBrowser : undefined}
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
suggestions={paths}
|
||||||
|
getSuggestionValue={getSuggestionValue}
|
||||||
|
renderSuggestion={renderSuggestion}
|
||||||
|
onInputKeyDown={handleInputKeyDown}
|
||||||
|
onInputChange={handleInputChange}
|
||||||
|
onInputBlur={handleInputBlur}
|
||||||
|
onSuggestionSelected={handleSuggestionSelected}
|
||||||
|
onSuggestionsFetchRequested={handleSuggestionsFetchRequested}
|
||||||
|
onSuggestionsClearRequested={handleSuggestionsClearRequested}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{hasFileBrowser ? (
|
||||||
|
<div>
|
||||||
|
<FormInputButton
|
||||||
|
className={styles.fileBrowserButton}
|
||||||
|
onPress={handleFileBrowserOpenPress}
|
||||||
|
>
|
||||||
|
<Icon name={icons.FOLDER_OPEN} />
|
||||||
|
</FormInputButton>
|
||||||
|
|
||||||
|
<FileBrowserModal
|
||||||
|
isOpen={isFileBrowserModalOpen}
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
includeFiles={includeFiles}
|
||||||
|
onChange={onChange}
|
||||||
|
onModalClose={handleFileBrowserModalClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { clearPaths, fetchPaths } from 'Store/Actions/pathActions';
|
|
||||||
import PathInput from './PathInput';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.paths,
|
|
||||||
(paths) => {
|
|
||||||
const {
|
|
||||||
currentPath,
|
|
||||||
directories,
|
|
||||||
files
|
|
||||||
} = paths;
|
|
||||||
|
|
||||||
const filteredPaths = _.filter([...directories, ...files], ({ path }) => {
|
|
||||||
return path.toLowerCase().startsWith(currentPath.toLowerCase());
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
paths: filteredPaths
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchFetchPaths: fetchPaths,
|
|
||||||
dispatchClearPaths: clearPaths
|
|
||||||
};
|
|
||||||
|
|
||||||
class PathInputConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onFetchPaths = (path) => {
|
|
||||||
const {
|
|
||||||
includeFiles,
|
|
||||||
dispatchFetchPaths
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
dispatchFetchPaths({
|
|
||||||
path,
|
|
||||||
includeFiles
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onClearPaths = () => {
|
|
||||||
this.props.dispatchClearPaths();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<PathInput
|
|
||||||
onFetchPaths={this.onFetchPaths}
|
|
||||||
onClearPaths={this.onClearPaths}
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PathInputConnector.propTypes = {
|
|
||||||
...PathInput.props,
|
|
||||||
includeFiles: PropTypes.bool.isRequired,
|
|
||||||
dispatchFetchPaths: PropTypes.func.isRequired,
|
|
||||||
dispatchClearPaths: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
PathInputConnector.defaultProps = {
|
|
||||||
includeFiles: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(PathInputConnector);
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createSortedSectionSelector('settings.qualityProfiles', sortByProp('name')),
|
|
||||||
(state, { includeNoChange }) => includeNoChange,
|
|
||||||
(state, { includeNoChangeDisabled }) => includeNoChangeDisabled,
|
|
||||||
(state, { includeMixed }) => includeMixed,
|
|
||||||
(qualityProfiles, includeNoChange, includeNoChangeDisabled = true, includeMixed) => {
|
|
||||||
const values = _.map(qualityProfiles.items, (qualityProfile) => {
|
|
||||||
return {
|
|
||||||
key: qualityProfile.id,
|
|
||||||
value: qualityProfile.name
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (includeNoChange) {
|
|
||||||
values.unshift({
|
|
||||||
key: 'noChange',
|
|
||||||
get value() {
|
|
||||||
return translate('NoChange');
|
|
||||||
},
|
|
||||||
isDisabled: includeNoChangeDisabled
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeMixed) {
|
|
||||||
values.unshift({
|
|
||||||
key: 'mixed',
|
|
||||||
get value() {
|
|
||||||
return `(${translate('Mixed')})`;
|
|
||||||
},
|
|
||||||
isDisabled: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
values
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class QualityProfileSelectInputConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
values
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!value || !values.some((option) => option.key === value || parseInt(option.key) === value)) {
|
|
||||||
const firstValue = values.find((option) => !isNaN(parseInt(option.key)));
|
|
||||||
|
|
||||||
if (firstValue) {
|
|
||||||
this.onChange({ name, value: firstValue.key });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onChange = ({ name, value }) => {
|
|
||||||
this.props.onChange({ name, value: value === 'noChange' ? value : parseInt(value) });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<EnhancedSelectInput
|
|
||||||
{...this.props}
|
|
||||||
onChange={this.onChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QualityProfileSelectInputConnector.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
|
||||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
includeNoChange: PropTypes.bool.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
QualityProfileSelectInputConnector.defaultProps = {
|
|
||||||
includeNoChange: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(QualityProfileSelectInputConnector);
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
|
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
|
||||||
import RootFolderSelectInputOption from './RootFolderSelectInputOption';
|
|
||||||
import RootFolderSelectInputSelectedValue from './RootFolderSelectInputSelectedValue';
|
|
||||||
|
|
||||||
class RootFolderSelectInput extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isAddNewRootFolderModalOpen: false,
|
|
||||||
newRootFolderPath: ''
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const newRootFolderPath = this.state.newRootFolderPath;
|
|
||||||
|
|
||||||
if (
|
|
||||||
prevProps.isSaving &&
|
|
||||||
!isSaving &&
|
|
||||||
!saveError &&
|
|
||||||
newRootFolderPath
|
|
||||||
) {
|
|
||||||
onChange({ name, value: newRootFolderPath });
|
|
||||||
this.setState({ newRootFolderPath: '' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onChange = ({ name, value }) => {
|
|
||||||
if (value === 'addNew') {
|
|
||||||
this.setState({ isAddNewRootFolderModalOpen: true });
|
|
||||||
} else {
|
|
||||||
this.props.onChange({ name, value });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onNewRootFolderSelect = ({ value }) => {
|
|
||||||
this.setState({ newRootFolderPath: value }, () => {
|
|
||||||
this.props.onNewRootFolderSelect(value);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onAddRootFolderModalClose = () => {
|
|
||||||
this.setState({ isAddNewRootFolderModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
includeNoChange,
|
|
||||||
onNewRootFolderSelect,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<EnhancedSelectInput
|
|
||||||
{...otherProps}
|
|
||||||
selectedValueComponent={RootFolderSelectInputSelectedValue}
|
|
||||||
optionComponent={RootFolderSelectInputOption}
|
|
||||||
onChange={this.onChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FileBrowserModal
|
|
||||||
isOpen={this.state.isAddNewRootFolderModalOpen}
|
|
||||||
name="rootFolderPath"
|
|
||||||
value=""
|
|
||||||
onChange={this.onNewRootFolderSelect}
|
|
||||||
onModalClose={this.onAddRootFolderModalClose}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RootFolderSelectInput.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
saveError: PropTypes.object,
|
|
||||||
includeNoChange: PropTypes.bool.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
onNewRootFolderSelect: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
RootFolderSelectInput.defaultProps = {
|
|
||||||
includeNoChange: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RootFolderSelectInput;
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { addRootFolder } from 'Store/Actions/rootFolderActions';
|
|
||||||
import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import RootFolderSelectInput from './RootFolderSelectInput';
|
|
||||||
|
|
||||||
const ADD_NEW_KEY = 'addNew';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createRootFoldersSelector(),
|
|
||||||
(state, { value }) => value,
|
|
||||||
(state, { includeMissingValue }) => includeMissingValue,
|
|
||||||
(state, { includeNoChange }) => includeNoChange,
|
|
||||||
(state, { includeNoChangeDisabled }) => includeNoChangeDisabled,
|
|
||||||
(rootFolders, value, includeMissingValue, includeNoChange, includeNoChangeDisabled = true) => {
|
|
||||||
const values = rootFolders.items.map((rootFolder) => {
|
|
||||||
return {
|
|
||||||
key: rootFolder.path,
|
|
||||||
value: rootFolder.path,
|
|
||||||
freeSpace: rootFolder.freeSpace,
|
|
||||||
isMissing: false
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (includeNoChange) {
|
|
||||||
values.unshift({
|
|
||||||
key: 'noChange',
|
|
||||||
get value() {
|
|
||||||
return translate('NoChange');
|
|
||||||
},
|
|
||||||
isDisabled: includeNoChangeDisabled,
|
|
||||||
isMissing: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!values.length) {
|
|
||||||
values.push({
|
|
||||||
key: '',
|
|
||||||
value: '',
|
|
||||||
isDisabled: true,
|
|
||||||
isHidden: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeMissingValue && !values.find((v) => v.key === value)) {
|
|
||||||
values.push({
|
|
||||||
key: value,
|
|
||||||
value,
|
|
||||||
isMissing: true,
|
|
||||||
isDisabled: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
values.push({
|
|
||||||
key: ADD_NEW_KEY,
|
|
||||||
value: translate('AddANewPath')
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
values,
|
|
||||||
isSaving: rootFolders.isSaving,
|
|
||||||
saveError: rootFolders.saveError
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
dispatchAddRootFolder(path) {
|
|
||||||
dispatch(addRootFolder({ path }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class RootFolderSelectInputConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentWillMount() {
|
|
||||||
const {
|
|
||||||
value,
|
|
||||||
values,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (value == null && values[0].key === '') {
|
|
||||||
onChange({ name, value: '' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
values,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!value || !values.some((v) => v.key === value) || value === ADD_NEW_KEY) {
|
|
||||||
const defaultValue = values[0];
|
|
||||||
|
|
||||||
if (defaultValue.key === ADD_NEW_KEY) {
|
|
||||||
onChange({ name, value: '' });
|
|
||||||
} else {
|
|
||||||
onChange({ name, value: defaultValue.key });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
values,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (prevProps.values === values) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!value && values.length && values.some((v) => !!v.key && v.key !== ADD_NEW_KEY)) {
|
|
||||||
const defaultValue = values[0];
|
|
||||||
|
|
||||||
if (defaultValue.key !== ADD_NEW_KEY) {
|
|
||||||
onChange({ name, value: defaultValue.key });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onNewRootFolderSelect = (path) => {
|
|
||||||
this.props.dispatchAddRootFolder(path);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
dispatchAddRootFolder,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RootFolderSelectInput
|
|
||||||
{...otherProps}
|
|
||||||
onNewRootFolderSelect={this.onNewRootFolderSelect}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RootFolderSelectInputConnector.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.string,
|
|
||||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
includeNoChange: PropTypes.bool.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
dispatchAddRootFolder: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
RootFolderSelectInputConnector.defaultProps = {
|
|
||||||
includeNoChange: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, createMapDispatchToProps)(RootFolderSelectInputConnector);
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import EnhancedSelectInputOption from './EnhancedSelectInputOption';
|
|
||||||
import styles from './RootFolderSelectInputOption.css';
|
|
||||||
|
|
||||||
function RootFolderSelectInputOption(props) {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
value,
|
|
||||||
freeSpace,
|
|
||||||
isMissing,
|
|
||||||
seriesFolder,
|
|
||||||
isMobile,
|
|
||||||
isWindows,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const slashCharacter = isWindows ? '\\' : '/';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EnhancedSelectInputOption
|
|
||||||
id={id}
|
|
||||||
isMobile={isMobile}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<div className={classNames(
|
|
||||||
styles.optionText,
|
|
||||||
isMobile && styles.isMobile
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className={styles.value}>
|
|
||||||
{value}
|
|
||||||
|
|
||||||
{
|
|
||||||
seriesFolder && id !== 'addNew' ?
|
|
||||||
<div className={styles.seriesFolder}>
|
|
||||||
{slashCharacter}
|
|
||||||
{seriesFolder}
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
freeSpace == null ?
|
|
||||||
null :
|
|
||||||
<div className={styles.freeSpace}>
|
|
||||||
{translate('RootFolderSelectFreeSpace', { freeSpace: formatBytes(freeSpace) })}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isMissing ?
|
|
||||||
<div className={styles.isMissing}>
|
|
||||||
{translate('Missing')}
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</EnhancedSelectInputOption>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
RootFolderSelectInputOption.propTypes = {
|
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.string.isRequired,
|
|
||||||
freeSpace: PropTypes.number,
|
|
||||||
isMissing: PropTypes.bool,
|
|
||||||
seriesFolder: PropTypes.string,
|
|
||||||
isMobile: PropTypes.bool.isRequired,
|
|
||||||
isWindows: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RootFolderSelectInputOption;
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue';
|
|
||||||
import styles from './RootFolderSelectInputSelectedValue.css';
|
|
||||||
|
|
||||||
function RootFolderSelectInputSelectedValue(props) {
|
|
||||||
const {
|
|
||||||
value,
|
|
||||||
freeSpace,
|
|
||||||
seriesFolder,
|
|
||||||
includeFreeSpace,
|
|
||||||
isWindows,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const slashCharacter = isWindows ? '\\' : '/';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EnhancedSelectInputSelectedValue
|
|
||||||
className={styles.selectedValue}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<div className={styles.pathContainer}>
|
|
||||||
<div className={styles.path}>
|
|
||||||
{value}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
seriesFolder ?
|
|
||||||
<div className={styles.seriesFolder}>
|
|
||||||
{slashCharacter}
|
|
||||||
{seriesFolder}
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
freeSpace != null && includeFreeSpace &&
|
|
||||||
<div className={styles.freeSpace}>
|
|
||||||
{translate('RootFolderSelectFreeSpace', { freeSpace: formatBytes(freeSpace) })}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</EnhancedSelectInputSelectedValue>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
RootFolderSelectInputSelectedValue.propTypes = {
|
|
||||||
value: PropTypes.string,
|
|
||||||
freeSpace: PropTypes.number,
|
|
||||||
seriesFolder: PropTypes.string,
|
|
||||||
isWindows: PropTypes.bool,
|
|
||||||
includeFreeSpace: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
RootFolderSelectInputSelectedValue.defaultProps = {
|
|
||||||
includeFreeSpace: true
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RootFolderSelectInputSelectedValue;
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
||||||
|
import { Protocol } from 'typings/DownloadClient';
|
||||||
|
import { EnhancedSelectInputChanged } from 'typings/inputs';
|
||||||
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import EnhancedSelectInput, {
|
||||||
|
EnhancedSelectInputProps,
|
||||||
|
EnhancedSelectInputValue,
|
||||||
|
} from './EnhancedSelectInput';
|
||||||
|
|
||||||
|
function createDownloadClientsSelector(
|
||||||
|
includeAny: boolean,
|
||||||
|
protocol: Protocol
|
||||||
|
) {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.settings.downloadClients,
|
||||||
|
(downloadClients) => {
|
||||||
|
const { isFetching, isPopulated, error, items } = downloadClients;
|
||||||
|
|
||||||
|
const filteredItems = items.filter((item) => item.protocol === protocol);
|
||||||
|
|
||||||
|
const values = filteredItems
|
||||||
|
.sort(sortByProp('name'))
|
||||||
|
.map((downloadClient) => {
|
||||||
|
return {
|
||||||
|
key: downloadClient.id,
|
||||||
|
value: downloadClient.name,
|
||||||
|
hint: `(${downloadClient.id})`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (includeAny) {
|
||||||
|
values.unshift({
|
||||||
|
key: 0,
|
||||||
|
value: `(${translate('Any')})`,
|
||||||
|
hint: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
values,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DownloadClientSelectInputProps
|
||||||
|
extends EnhancedSelectInputProps<EnhancedSelectInputValue<number>, number> {
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
includeAny?: boolean;
|
||||||
|
protocol?: Protocol;
|
||||||
|
onChange: (change: EnhancedSelectInputChanged<number>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DownloadClientSelectInput({
|
||||||
|
includeAny = false,
|
||||||
|
protocol = 'torrent',
|
||||||
|
...otherProps
|
||||||
|
}: DownloadClientSelectInputProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { isFetching, isPopulated, values } = useSelector(
|
||||||
|
createDownloadClientsSelector(includeAny, protocol)
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isPopulated) {
|
||||||
|
dispatch(fetchDownloadClients());
|
||||||
|
}
|
||||||
|
}, [isPopulated, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EnhancedSelectInput
|
||||||
|
{...otherProps}
|
||||||
|
isFetching={isFetching}
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DownloadClientSelectInput;
|
||||||
+6
@@ -73,6 +73,12 @@
|
|||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.optionsInnerModalBody {
|
||||||
|
composes: innerModalBody from '~Components/Modal/ModalBody.css';
|
||||||
|
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.optionsModalScroller {
|
.optionsModalScroller {
|
||||||
composes: scroller from '~Components/Scroller/Scroller.css';
|
composes: scroller from '~Components/Scroller/Scroller.css';
|
||||||
|
|
||||||
+1
@@ -14,6 +14,7 @@ interface CssExports {
|
|||||||
'mobileCloseButtonContainer': string;
|
'mobileCloseButtonContainer': string;
|
||||||
'options': string;
|
'options': string;
|
||||||
'optionsContainer': string;
|
'optionsContainer': string;
|
||||||
|
'optionsInnerModalBody': string;
|
||||||
'optionsModal': string;
|
'optionsModal': string;
|
||||||
'optionsModalBody': string;
|
'optionsModalBody': string;
|
||||||
'optionsModalScroller': string;
|
'optionsModalScroller': string;
|
||||||
@@ -0,0 +1,622 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, {
|
||||||
|
ElementType,
|
||||||
|
KeyboardEvent,
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { Manager, Popper, Reference } from 'react-popper';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import Measure from 'Components/Measure';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import Portal from 'Components/Portal';
|
||||||
|
import Scroller from 'Components/Scroller/Scroller';
|
||||||
|
import { icons, scrollDirections, sizes } from 'Helpers/Props';
|
||||||
|
import ArrayElement from 'typings/Helpers/ArrayElement';
|
||||||
|
import { EnhancedSelectInputChanged, InputChanged } from 'typings/inputs';
|
||||||
|
import { isMobile as isMobileUtil } from 'Utilities/browser';
|
||||||
|
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||||
|
import getUniqueElementId from 'Utilities/getUniqueElementId';
|
||||||
|
import TextInput from '../TextInput';
|
||||||
|
import HintedSelectInputOption from './HintedSelectInputOption';
|
||||||
|
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
||||||
|
import styles from './EnhancedSelectInput.css';
|
||||||
|
|
||||||
|
function isArrowKey(keyCode: number) {
|
||||||
|
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedOption<T extends EnhancedSelectInputValue<V>, V>(
|
||||||
|
selectedIndex: number,
|
||||||
|
values: T[]
|
||||||
|
) {
|
||||||
|
return values[selectedIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
function findIndex<T extends EnhancedSelectInputValue<V>, V>(
|
||||||
|
startingIndex: number,
|
||||||
|
direction: 1 | -1,
|
||||||
|
values: T[]
|
||||||
|
) {
|
||||||
|
let indexToTest = startingIndex + direction;
|
||||||
|
|
||||||
|
while (indexToTest !== startingIndex) {
|
||||||
|
if (indexToTest < 0) {
|
||||||
|
indexToTest = values.length - 1;
|
||||||
|
} else if (indexToTest >= values.length) {
|
||||||
|
indexToTest = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getSelectedOption(indexToTest, values).isDisabled) {
|
||||||
|
indexToTest = indexToTest + direction;
|
||||||
|
} else {
|
||||||
|
return indexToTest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function previousIndex<T extends EnhancedSelectInputValue<V>, V>(
|
||||||
|
selectedIndex: number,
|
||||||
|
values: T[]
|
||||||
|
) {
|
||||||
|
return findIndex(selectedIndex, -1, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextIndex<T extends EnhancedSelectInputValue<V>, V>(
|
||||||
|
selectedIndex: number,
|
||||||
|
values: T[]
|
||||||
|
) {
|
||||||
|
return findIndex(selectedIndex, 1, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedIndex<T extends EnhancedSelectInputValue<V>, V>(
|
||||||
|
value: V,
|
||||||
|
values: T[]
|
||||||
|
) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return values.findIndex((v) => {
|
||||||
|
return v.key === value[0];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return values.findIndex((v) => {
|
||||||
|
return v.key === value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSelectedItem<T extends EnhancedSelectInputValue<V>, V>(
|
||||||
|
index: number,
|
||||||
|
value: V,
|
||||||
|
values: T[]
|
||||||
|
) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.includes(values[index].key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return values[index].key === value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnhancedSelectInputValue<V> {
|
||||||
|
key: ArrayElement<V>;
|
||||||
|
value: string;
|
||||||
|
hint?: ReactNode;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
isHidden?: boolean;
|
||||||
|
parentKey?: V;
|
||||||
|
additionalProperties?: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnhancedSelectInputProps<
|
||||||
|
T extends EnhancedSelectInputValue<V>,
|
||||||
|
V
|
||||||
|
> {
|
||||||
|
className?: string;
|
||||||
|
disabledClassName?: string;
|
||||||
|
name: string;
|
||||||
|
value: V;
|
||||||
|
values: T[];
|
||||||
|
isDisabled?: boolean;
|
||||||
|
isFetching?: boolean;
|
||||||
|
isEditable?: boolean;
|
||||||
|
hasError?: boolean;
|
||||||
|
hasWarning?: boolean;
|
||||||
|
valueOptions?: object;
|
||||||
|
selectedValueOptions?: object;
|
||||||
|
selectedValueComponent?: string | ElementType;
|
||||||
|
optionComponent?: ElementType;
|
||||||
|
onOpen?: () => void;
|
||||||
|
onChange: (change: EnhancedSelectInputChanged<V>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>(
|
||||||
|
props: EnhancedSelectInputProps<T, V>
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
className = styles.enhancedSelect,
|
||||||
|
disabledClassName = styles.isDisabled,
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
values,
|
||||||
|
isDisabled = false,
|
||||||
|
isEditable,
|
||||||
|
isFetching,
|
||||||
|
hasError,
|
||||||
|
hasWarning,
|
||||||
|
valueOptions,
|
||||||
|
selectedValueOptions,
|
||||||
|
selectedValueComponent:
|
||||||
|
SelectedValueComponent = HintedSelectInputSelectedValue,
|
||||||
|
optionComponent: OptionComponent = HintedSelectInputOption,
|
||||||
|
onChange,
|
||||||
|
onOpen,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const updater = useRef<(() => void) | null>(null);
|
||||||
|
const buttonId = useMemo(() => getUniqueElementId(), []);
|
||||||
|
const optionsId = useMemo(() => getUniqueElementId(), []);
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState(
|
||||||
|
getSelectedIndex(value, values)
|
||||||
|
);
|
||||||
|
const [width, setWidth] = useState(0);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const isMobile = useMemo(() => isMobileUtil(), []);
|
||||||
|
|
||||||
|
const isMultiSelect = Array.isArray(value);
|
||||||
|
const selectedOption = getSelectedOption(selectedIndex, values);
|
||||||
|
|
||||||
|
const selectedValue = useMemo(() => {
|
||||||
|
if (values.length) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMultiSelect) {
|
||||||
|
return [];
|
||||||
|
} else if (typeof value === 'number') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}, [value, values, isMultiSelect]);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const handleComputeMaxHeight = useCallback((data: any) => {
|
||||||
|
const { top, bottom } = data.offsets.reference;
|
||||||
|
const windowHeight = window.innerHeight;
|
||||||
|
|
||||||
|
if (/^botton/.test(data.placement)) {
|
||||||
|
data.styles.maxHeight = windowHeight - bottom;
|
||||||
|
} else {
|
||||||
|
data.styles.maxHeight = top;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleWindowClick = useCallback(
|
||||||
|
(event: MouseEvent) => {
|
||||||
|
const button = document.getElementById(buttonId);
|
||||||
|
const options = document.getElementById(optionsId);
|
||||||
|
const eventTarget = event.target as HTMLElement;
|
||||||
|
|
||||||
|
if (!button || !eventTarget.isConnected || isMobile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!button.contains(eventTarget) &&
|
||||||
|
options &&
|
||||||
|
!options.contains(eventTarget) &&
|
||||||
|
isOpen
|
||||||
|
) {
|
||||||
|
setIsOpen(false);
|
||||||
|
window.removeEventListener('click', handleWindowClick);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isMobile, isOpen, buttonId, optionsId, setIsOpen]
|
||||||
|
);
|
||||||
|
|
||||||
|
const addListener = useCallback(() => {
|
||||||
|
window.addEventListener('click', handleWindowClick);
|
||||||
|
}, [handleWindowClick]);
|
||||||
|
|
||||||
|
const removeListener = useCallback(() => {
|
||||||
|
window.removeEventListener('click', handleWindowClick);
|
||||||
|
}, [handleWindowClick]);
|
||||||
|
|
||||||
|
const handlePress = useCallback(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
removeListener();
|
||||||
|
} else {
|
||||||
|
addListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isOpen && onOpen) {
|
||||||
|
onOpen();
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
}, [isOpen, setIsOpen, addListener, removeListener, onOpen]);
|
||||||
|
|
||||||
|
const handleSelect = useCallback(
|
||||||
|
(newValue: ArrayElement<V>) => {
|
||||||
|
const additionalProperties = values.find(
|
||||||
|
(v) => v.key === newValue
|
||||||
|
)?.additionalProperties;
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const index = value.indexOf(newValue);
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
const arrayValue = values
|
||||||
|
.map((v) => v.key)
|
||||||
|
.filter((v) => v === newValue || value.includes(v));
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: arrayValue as V,
|
||||||
|
additionalProperties,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const arrayValue = [...value];
|
||||||
|
arrayValue.splice(index, 1);
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: arrayValue as V,
|
||||||
|
additionalProperties,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setIsOpen(false);
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: newValue as V,
|
||||||
|
additionalProperties,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[name, value, values, onChange, setIsOpen]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleBlur = useCallback(() => {
|
||||||
|
if (!isEditable) {
|
||||||
|
// Calling setState without this check prevents the click event from being properly handled on Chrome (it is on firefox)
|
||||||
|
const origIndex = getSelectedIndex(value, values);
|
||||||
|
|
||||||
|
if (origIndex !== selectedIndex) {
|
||||||
|
setSelectedIndex(origIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [value, values, isEditable, selectedIndex, setSelectedIndex]);
|
||||||
|
|
||||||
|
const handleFocus = useCallback(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
removeListener();
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
}, [isOpen, setIsOpen, removeListener]);
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback(
|
||||||
|
(event: KeyboardEvent<HTMLButtonElement>) => {
|
||||||
|
const keyCode = event.keyCode;
|
||||||
|
let nextIsOpen: boolean | null = null;
|
||||||
|
let nextSelectedIndex: number | null = null;
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
if (isArrowKey(keyCode)) {
|
||||||
|
event.preventDefault();
|
||||||
|
nextIsOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
selectedIndex == null ||
|
||||||
|
selectedIndex === -1 ||
|
||||||
|
getSelectedOption(selectedIndex, values).isDisabled
|
||||||
|
) {
|
||||||
|
if (keyCode === keyCodes.UP_ARROW) {
|
||||||
|
nextSelectedIndex = previousIndex(0, values);
|
||||||
|
} else if (keyCode === keyCodes.DOWN_ARROW) {
|
||||||
|
nextSelectedIndex = nextIndex(values.length - 1, values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextIsOpen !== null) {
|
||||||
|
setIsOpen(nextIsOpen);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextSelectedIndex !== null) {
|
||||||
|
setSelectedIndex(nextSelectedIndex);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyCode === keyCodes.UP_ARROW) {
|
||||||
|
event.preventDefault();
|
||||||
|
nextSelectedIndex = previousIndex(selectedIndex, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyCode === keyCodes.DOWN_ARROW) {
|
||||||
|
event.preventDefault();
|
||||||
|
nextSelectedIndex = nextIndex(selectedIndex, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyCode === keyCodes.ENTER) {
|
||||||
|
event.preventDefault();
|
||||||
|
nextIsOpen = false;
|
||||||
|
handleSelect(values[selectedIndex].key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyCode === keyCodes.TAB) {
|
||||||
|
nextIsOpen = false;
|
||||||
|
handleSelect(values[selectedIndex].key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyCode === keyCodes.ESCAPE) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
nextIsOpen = false;
|
||||||
|
nextSelectedIndex = getSelectedIndex(value, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextIsOpen !== null) {
|
||||||
|
setIsOpen(nextIsOpen);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextSelectedIndex !== null) {
|
||||||
|
setSelectedIndex(nextSelectedIndex);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
value,
|
||||||
|
isOpen,
|
||||||
|
selectedIndex,
|
||||||
|
values,
|
||||||
|
setIsOpen,
|
||||||
|
setSelectedIndex,
|
||||||
|
handleSelect,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMeasure = useCallback(
|
||||||
|
({ width: newWidth }: { width: number }) => {
|
||||||
|
setWidth(newWidth);
|
||||||
|
},
|
||||||
|
[setWidth]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOptionsModalClose = useCallback(() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
}, [setIsOpen]);
|
||||||
|
|
||||||
|
const handleEditChange = useCallback(
|
||||||
|
(change: InputChanged<string>) => {
|
||||||
|
onChange(change as EnhancedSelectInputChanged<V>);
|
||||||
|
},
|
||||||
|
[onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (updater.current) {
|
||||||
|
updater.current();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Manager>
|
||||||
|
<Reference>
|
||||||
|
{({ ref }) => (
|
||||||
|
<div ref={ref} id={buttonId}>
|
||||||
|
<Measure whitelist={['width']} onMeasure={handleMeasure}>
|
||||||
|
{isEditable && typeof value === 'string' ? (
|
||||||
|
<div className={styles.editableContainer}>
|
||||||
|
<TextInput
|
||||||
|
className={className}
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
readOnly={isDisabled}
|
||||||
|
hasError={hasError}
|
||||||
|
hasWarning={hasWarning}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onChange={handleEditChange}
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
className={classNames(
|
||||||
|
styles.dropdownArrowContainerEditable,
|
||||||
|
isDisabled
|
||||||
|
? styles.dropdownArrowContainerDisabled
|
||||||
|
: styles.dropdownArrowContainer
|
||||||
|
)}
|
||||||
|
onPress={handlePress}
|
||||||
|
>
|
||||||
|
{isFetching ? (
|
||||||
|
<LoadingIndicator
|
||||||
|
className={styles.loading}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isFetching ? null : <Icon name={icons.CARET_DOWN} />}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
className={classNames(
|
||||||
|
className,
|
||||||
|
hasError && styles.hasError,
|
||||||
|
hasWarning && styles.hasWarning,
|
||||||
|
isDisabled && disabledClassName
|
||||||
|
)}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onPress={handlePress}
|
||||||
|
>
|
||||||
|
<SelectedValueComponent
|
||||||
|
values={values}
|
||||||
|
{...selectedValueOptions}
|
||||||
|
selectedValue={selectedValue}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
isMultiSelect={isMultiSelect}
|
||||||
|
>
|
||||||
|
{selectedOption ? selectedOption.value : selectedValue}
|
||||||
|
</SelectedValueComponent>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
isDisabled
|
||||||
|
? styles.dropdownArrowContainerDisabled
|
||||||
|
: styles.dropdownArrowContainer
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isFetching ? (
|
||||||
|
<LoadingIndicator
|
||||||
|
className={styles.loading}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isFetching ? null : <Icon name={icons.CARET_DOWN} />}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</Measure>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Reference>
|
||||||
|
<Portal>
|
||||||
|
<Popper
|
||||||
|
placement="bottom-start"
|
||||||
|
modifiers={{
|
||||||
|
computeMaxHeight: {
|
||||||
|
order: 851,
|
||||||
|
enabled: true,
|
||||||
|
fn: handleComputeMaxHeight,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ ref, style, scheduleUpdate }) => {
|
||||||
|
updater.current = scheduleUpdate;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
id={optionsId}
|
||||||
|
className={styles.optionsContainer}
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
minWidth: width,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isOpen && !isMobile ? (
|
||||||
|
<Scroller
|
||||||
|
className={styles.options}
|
||||||
|
style={{
|
||||||
|
maxHeight: style.maxHeight,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{values.map((v, index) => {
|
||||||
|
const hasParent = v.parentKey !== undefined;
|
||||||
|
const depth = hasParent ? 1 : 0;
|
||||||
|
const parentSelected =
|
||||||
|
v.parentKey !== undefined &&
|
||||||
|
Array.isArray(value) &&
|
||||||
|
value.includes(v.parentKey);
|
||||||
|
|
||||||
|
const { key, ...other } = v;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OptionComponent
|
||||||
|
key={v.key}
|
||||||
|
id={v.key}
|
||||||
|
depth={depth}
|
||||||
|
isSelected={isSelectedItem(index, value, values)}
|
||||||
|
isDisabled={parentSelected}
|
||||||
|
isMultiSelect={isMultiSelect}
|
||||||
|
{...valueOptions}
|
||||||
|
{...other}
|
||||||
|
isMobile={false}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
>
|
||||||
|
{v.value}
|
||||||
|
</OptionComponent>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Scroller>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Popper>
|
||||||
|
</Portal>
|
||||||
|
</Manager>
|
||||||
|
|
||||||
|
{isMobile ? (
|
||||||
|
<Modal
|
||||||
|
className={styles.optionsModal}
|
||||||
|
size={sizes.EXTRA_SMALL}
|
||||||
|
isOpen={isOpen}
|
||||||
|
onModalClose={handleOptionsModalClose}
|
||||||
|
>
|
||||||
|
<ModalBody
|
||||||
|
className={styles.optionsModalBody}
|
||||||
|
innerClassName={styles.optionsInnerModalBody}
|
||||||
|
scrollDirection={scrollDirections.NONE}
|
||||||
|
>
|
||||||
|
<Scroller className={styles.optionsModalScroller}>
|
||||||
|
<div className={styles.mobileCloseButtonContainer}>
|
||||||
|
<Link
|
||||||
|
className={styles.mobileCloseButton}
|
||||||
|
onPress={handleOptionsModalClose}
|
||||||
|
>
|
||||||
|
<Icon name={icons.CLOSE} size={18} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{values.map((v, index) => {
|
||||||
|
const hasParent = v.parentKey !== undefined;
|
||||||
|
const depth = hasParent ? 1 : 0;
|
||||||
|
const parentSelected =
|
||||||
|
v.parentKey !== undefined &&
|
||||||
|
isMultiSelect &&
|
||||||
|
value.includes(v.parentKey);
|
||||||
|
|
||||||
|
const { key, ...other } = v;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OptionComponent
|
||||||
|
key={key}
|
||||||
|
id={key}
|
||||||
|
depth={depth}
|
||||||
|
isSelected={isSelectedItem(index, value, values)}
|
||||||
|
isMultiSelect={isMultiSelect}
|
||||||
|
isDisabled={parentSelected}
|
||||||
|
{...valueOptions}
|
||||||
|
{...other}
|
||||||
|
isMobile={true}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
>
|
||||||
|
{v.value}
|
||||||
|
</OptionComponent>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Scroller>
|
||||||
|
</ModalBody>
|
||||||
|
</Modal>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EnhancedSelectInput;
|
||||||
+2
-2
@@ -16,13 +16,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.optionCheck {
|
.optionCheck {
|
||||||
composes: container from '~./CheckInput.css';
|
composes: container from '~Components/Form/CheckInput.css';
|
||||||
|
|
||||||
flex: 0 0 0;
|
flex: 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.optionCheckInput {
|
.optionCheckInput {
|
||||||
composes: input from '~./CheckInput.css';
|
composes: input from '~Components/Form/CheckInput.css';
|
||||||
|
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { SyntheticEvent, useCallback } from 'react';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import CheckInput from '../CheckInput';
|
||||||
|
import styles from './EnhancedSelectInputOption.css';
|
||||||
|
|
||||||
|
function handleCheckPress() {
|
||||||
|
// CheckInput requires a handler. Swallow the change event because onPress will already handle it via event propagation.
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnhancedSelectInputOptionProps {
|
||||||
|
className?: string;
|
||||||
|
id: string | number;
|
||||||
|
depth?: number;
|
||||||
|
isSelected: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
isHidden?: boolean;
|
||||||
|
isMultiSelect?: boolean;
|
||||||
|
isMobile: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
onSelect: (...args: unknown[]) => unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EnhancedSelectInputOption({
|
||||||
|
className = styles.option,
|
||||||
|
id,
|
||||||
|
depth = 0,
|
||||||
|
isSelected,
|
||||||
|
isDisabled = false,
|
||||||
|
isHidden = false,
|
||||||
|
isMultiSelect = false,
|
||||||
|
isMobile,
|
||||||
|
children,
|
||||||
|
onSelect,
|
||||||
|
}: EnhancedSelectInputOptionProps) {
|
||||||
|
const handlePress = useCallback(
|
||||||
|
(event: SyntheticEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
onSelect(id);
|
||||||
|
},
|
||||||
|
[id, onSelect]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className={classNames(
|
||||||
|
className,
|
||||||
|
isSelected && !isMultiSelect && styles.isSelected,
|
||||||
|
isDisabled && !isMultiSelect && styles.isDisabled,
|
||||||
|
isHidden && styles.isHidden,
|
||||||
|
isMobile && styles.isMobile
|
||||||
|
)}
|
||||||
|
component="div"
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
onPress={handlePress}
|
||||||
|
>
|
||||||
|
{depth !== 0 && <div style={{ width: `${depth * 20}px` }} />}
|
||||||
|
|
||||||
|
{isMultiSelect && (
|
||||||
|
<CheckInput
|
||||||
|
className={styles.optionCheckInput}
|
||||||
|
containerClassName={styles.optionCheck}
|
||||||
|
name={`select-${id}`}
|
||||||
|
value={isSelected}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
onChange={handleCheckPress}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
|
||||||
|
{isMobile && (
|
||||||
|
<div className={styles.iconContainer}>
|
||||||
|
<Icon name={isSelected ? icons.CHECK_CIRCLE : icons.CIRCLE_OUTLINE} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EnhancedSelectInputOption;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import styles from './EnhancedSelectInputSelectedValue.css';
|
||||||
|
|
||||||
|
interface EnhancedSelectInputSelectedValueProps {
|
||||||
|
className?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EnhancedSelectInputSelectedValue({
|
||||||
|
className = styles.selectedValue,
|
||||||
|
children,
|
||||||
|
isDisabled = false,
|
||||||
|
}: EnhancedSelectInputSelectedValueProps) {
|
||||||
|
return (
|
||||||
|
<div className={classNames(className, isDisabled && styles.isDisabled)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EnhancedSelectInputSelectedValue;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import EnhancedSelectInputOption, {
|
||||||
|
EnhancedSelectInputOptionProps,
|
||||||
|
} from './EnhancedSelectInputOption';
|
||||||
|
import styles from './HintedSelectInputOption.css';
|
||||||
|
|
||||||
|
interface HintedSelectInputOptionProps extends EnhancedSelectInputOptionProps {
|
||||||
|
value: string;
|
||||||
|
hint?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HintedSelectInputOption(props: HintedSelectInputOptionProps) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
value,
|
||||||
|
hint,
|
||||||
|
depth,
|
||||||
|
isSelected = false,
|
||||||
|
isDisabled,
|
||||||
|
isMobile,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EnhancedSelectInputOption
|
||||||
|
id={id}
|
||||||
|
depth={depth}
|
||||||
|
isSelected={isSelected}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
isHidden={isDisabled}
|
||||||
|
isMobile={isMobile}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={classNames(styles.optionText, isMobile && styles.isMobile)}
|
||||||
|
>
|
||||||
|
<div>{value}</div>
|
||||||
|
|
||||||
|
{hint != null && <div className={styles.hintText}>{hint}</div>}
|
||||||
|
</div>
|
||||||
|
</EnhancedSelectInputOption>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
HintedSelectInputOption.defaultProps = {
|
||||||
|
isDisabled: false,
|
||||||
|
isHidden: false,
|
||||||
|
isMultiSelect: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HintedSelectInputOption;
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import React, { ReactNode, useMemo } from 'react';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import ArrayElement from 'typings/Helpers/ArrayElement';
|
||||||
|
import { EnhancedSelectInputValue } from './EnhancedSelectInput';
|
||||||
|
import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue';
|
||||||
|
import styles from './HintedSelectInputSelectedValue.css';
|
||||||
|
|
||||||
|
interface HintedSelectInputSelectedValueProps<T, V> {
|
||||||
|
selectedValue: V;
|
||||||
|
values: T[];
|
||||||
|
hint?: ReactNode;
|
||||||
|
isMultiSelect?: boolean;
|
||||||
|
includeHint?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HintedSelectInputSelectedValue<
|
||||||
|
T extends EnhancedSelectInputValue<V>,
|
||||||
|
V extends number | string
|
||||||
|
>(props: HintedSelectInputSelectedValueProps<T, V>) {
|
||||||
|
const {
|
||||||
|
selectedValue,
|
||||||
|
values,
|
||||||
|
hint,
|
||||||
|
isMultiSelect = false,
|
||||||
|
includeHint = true,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const valuesMap = useMemo(() => {
|
||||||
|
return new Map(values.map((v) => [v.key, v.value]));
|
||||||
|
}, [values]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EnhancedSelectInputSelectedValue
|
||||||
|
className={styles.selectedValue}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
<div className={styles.valueText}>
|
||||||
|
{isMultiSelect && Array.isArray(selectedValue)
|
||||||
|
? selectedValue.map((key) => {
|
||||||
|
const v = valuesMap.get(key);
|
||||||
|
|
||||||
|
return <Label key={key}>{v ? v : key}</Label>;
|
||||||
|
})
|
||||||
|
: valuesMap.get(selectedValue as ArrayElement<V>)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hint != null && includeHint ? (
|
||||||
|
<div className={styles.hintText}>{hint}</div>
|
||||||
|
) : null}
|
||||||
|
</EnhancedSelectInputSelectedValue>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HintedSelectInputSelectedValue;
|
||||||
+18
-10
@@ -2,6 +2,7 @@ import React, { useCallback } from 'react';
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import AppState from 'App/State/AppState';
|
import AppState from 'App/State/AppState';
|
||||||
|
import { EnhancedSelectInputChanged } from 'typings/inputs';
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
const selectIndexerFlagsValues = (selectedFlags: number) =>
|
const selectIndexerFlagsValues = (selectedFlags: number) =>
|
||||||
@@ -32,29 +33,36 @@ const selectIndexerFlagsValues = (selectedFlags: number) =>
|
|||||||
interface IndexerFlagsSelectInputProps {
|
interface IndexerFlagsSelectInputProps {
|
||||||
name: string;
|
name: string;
|
||||||
indexerFlags: number;
|
indexerFlags: number;
|
||||||
onChange(payload: object): void;
|
onChange(payload: EnhancedSelectInputChanged<number>): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function IndexerFlagsSelectInput(props: IndexerFlagsSelectInputProps) {
|
function IndexerFlagsSelectInput({
|
||||||
const { indexerFlags, onChange } = props;
|
name,
|
||||||
|
indexerFlags,
|
||||||
|
onChange,
|
||||||
|
...otherProps
|
||||||
|
}: IndexerFlagsSelectInputProps) {
|
||||||
const { value, values } = useSelector(selectIndexerFlagsValues(indexerFlags));
|
const { value, values } = useSelector(selectIndexerFlagsValues(indexerFlags));
|
||||||
|
|
||||||
const onChangeWrapper = useCallback(
|
const handleChange = useCallback(
|
||||||
({ name, value }: { name: string; value: number[] }) => {
|
(change: EnhancedSelectInputChanged<number[]>) => {
|
||||||
const indexerFlags = value.reduce((acc, flagId) => acc + flagId, 0);
|
const indexerFlags = change.value.reduce(
|
||||||
|
(acc, flagId) => acc + flagId,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
onChange({ name, value: indexerFlags });
|
onChange({ name, value: indexerFlags });
|
||||||
},
|
},
|
||||||
[onChange]
|
[name, onChange]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedSelectInput
|
<EnhancedSelectInput
|
||||||
{...props}
|
{...otherProps}
|
||||||
|
name={name}
|
||||||
value={value}
|
value={value}
|
||||||
values={values}
|
values={values}
|
||||||
onChange={onChangeWrapper}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import { fetchIndexers } from 'Store/Actions/settingsActions';
|
||||||
|
import { EnhancedSelectInputChanged } from 'typings/inputs';
|
||||||
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
|
function createIndexersSelector(includeAny: boolean) {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.settings.indexers,
|
||||||
|
(indexers) => {
|
||||||
|
const { isFetching, isPopulated, error, items } = indexers;
|
||||||
|
|
||||||
|
const values = items.sort(sortByProp('name')).map((indexer) => {
|
||||||
|
return {
|
||||||
|
key: indexer.id,
|
||||||
|
value: indexer.name,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (includeAny) {
|
||||||
|
values.unshift({
|
||||||
|
key: 0,
|
||||||
|
value: `(${translate('Any')})`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
values,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IndexerSelectInputConnectorProps {
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
includeAny?: boolean;
|
||||||
|
values: object[];
|
||||||
|
onChange: (change: EnhancedSelectInputChanged<number>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function IndexerSelectInput({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
includeAny = false,
|
||||||
|
onChange,
|
||||||
|
}: IndexerSelectInputConnectorProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { isFetching, isPopulated, values } = useSelector(
|
||||||
|
createIndexersSelector(includeAny)
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isPopulated) {
|
||||||
|
dispatch(fetchIndexers());
|
||||||
|
}
|
||||||
|
}, [isPopulated, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EnhancedSelectInput
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
isFetching={isFetching}
|
||||||
|
values={values}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
IndexerSelectInput.defaultProps = {
|
||||||
|
includeAny: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IndexerSelectInput;
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { EnhancedSelectInputChanged } from 'typings/inputs';
|
||||||
|
import EnhancedSelectInput, {
|
||||||
|
EnhancedSelectInputValue,
|
||||||
|
} from './EnhancedSelectInput';
|
||||||
|
|
||||||
|
interface LanguageSelectInputProps {
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
values: EnhancedSelectInputValue<number>[];
|
||||||
|
onChange: (change: EnhancedSelectInputChanged<number>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LanguageSelectInput({
|
||||||
|
values,
|
||||||
|
onChange,
|
||||||
|
...otherProps
|
||||||
|
}: LanguageSelectInputProps) {
|
||||||
|
const mappedValues = useMemo(() => {
|
||||||
|
const minId = values.reduce(
|
||||||
|
(min: number, v) => (v.key < 1 ? v.key : min),
|
||||||
|
values[0].key
|
||||||
|
);
|
||||||
|
|
||||||
|
return values.map(({ key, value }) => {
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
dividerAfter: minId < 1 ? key === minId : false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [values]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EnhancedSelectInput
|
||||||
|
{...otherProps}
|
||||||
|
values={mappedValues}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LanguageSelectInput;
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import monitorOptions from 'Utilities/Series/monitorOptions';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import EnhancedSelectInput, {
|
||||||
|
EnhancedSelectInputProps,
|
||||||
|
EnhancedSelectInputValue,
|
||||||
|
} from './EnhancedSelectInput';
|
||||||
|
|
||||||
|
interface MonitorEpisodesSelectInputProps
|
||||||
|
extends Omit<
|
||||||
|
EnhancedSelectInputProps<EnhancedSelectInputValue<string>, string>,
|
||||||
|
'values'
|
||||||
|
> {
|
||||||
|
includeNoChange: boolean;
|
||||||
|
includeMixed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MonitorEpisodesSelectInput(props: MonitorEpisodesSelectInputProps) {
|
||||||
|
const {
|
||||||
|
includeNoChange = false,
|
||||||
|
includeMixed = false,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const values: EnhancedSelectInputValue<string>[] = [...monitorOptions];
|
||||||
|
|
||||||
|
if (includeNoChange) {
|
||||||
|
values.unshift({
|
||||||
|
key: 'noChange',
|
||||||
|
get value() {
|
||||||
|
return translate('NoChange');
|
||||||
|
},
|
||||||
|
isDisabled: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeMixed) {
|
||||||
|
values.unshift({
|
||||||
|
key: 'mixed',
|
||||||
|
get value() {
|
||||||
|
return `(${translate('Mixed')})`;
|
||||||
|
},
|
||||||
|
isDisabled: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return <EnhancedSelectInput {...otherProps} values={values} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MonitorEpisodesSelectInput;
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import monitorNewItemsOptions from 'Utilities/Series/monitorNewItemsOptions';
|
||||||
|
import EnhancedSelectInput, {
|
||||||
|
EnhancedSelectInputProps,
|
||||||
|
EnhancedSelectInputValue,
|
||||||
|
} from './EnhancedSelectInput';
|
||||||
|
|
||||||
|
interface MonitorNewItemsSelectInputProps
|
||||||
|
extends Omit<
|
||||||
|
EnhancedSelectInputProps<EnhancedSelectInputValue<string>, string>,
|
||||||
|
'values'
|
||||||
|
> {
|
||||||
|
includeNoChange?: boolean;
|
||||||
|
includeMixed?: boolean;
|
||||||
|
onChange: (...args: unknown[]) => unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MonitorNewItemsSelectInput(props: MonitorNewItemsSelectInputProps) {
|
||||||
|
const {
|
||||||
|
includeNoChange = false,
|
||||||
|
includeMixed = false,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const values: EnhancedSelectInputValue<string>[] = [
|
||||||
|
...monitorNewItemsOptions,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (includeNoChange) {
|
||||||
|
values.unshift({
|
||||||
|
key: 'noChange',
|
||||||
|
value: 'No Change',
|
||||||
|
isDisabled: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeMixed) {
|
||||||
|
values.unshift({
|
||||||
|
key: 'mixed',
|
||||||
|
value: '(Mixed)',
|
||||||
|
isDisabled: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return <EnhancedSelectInput {...otherProps} values={values} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MonitorNewItemsSelectInput;
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import { isEqual } from 'lodash';
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import ProviderOptionsAppState, {
|
||||||
|
ProviderOptions,
|
||||||
|
} from 'App/State/ProviderOptionsAppState';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import {
|
||||||
|
clearOptions,
|
||||||
|
fetchOptions,
|
||||||
|
} from 'Store/Actions/providerOptionActions';
|
||||||
|
import { FieldSelectOption } from 'typings/Field';
|
||||||
|
import EnhancedSelectInput, {
|
||||||
|
EnhancedSelectInputProps,
|
||||||
|
EnhancedSelectInputValue,
|
||||||
|
} from './EnhancedSelectInput';
|
||||||
|
|
||||||
|
const importantFieldNames = ['baseUrl', 'apiPath', 'apiKey', 'authToken'];
|
||||||
|
|
||||||
|
function getProviderDataKey(providerData: ProviderOptions) {
|
||||||
|
if (!providerData || !providerData.fields) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = providerData.fields
|
||||||
|
.filter((f) => importantFieldNames.includes(f.name))
|
||||||
|
.map((f) => f.value);
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectOptions(items: FieldSelectOption<unknown>[]) {
|
||||||
|
if (!items) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.map((option) => {
|
||||||
|
return {
|
||||||
|
key: option.value,
|
||||||
|
value: option.name,
|
||||||
|
hint: option.hint,
|
||||||
|
parentKey: option.parentValue,
|
||||||
|
isDisabled: option.isDisabled,
|
||||||
|
additionalProperties: option.additionalProperties,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createProviderOptionsSelector(
|
||||||
|
selectOptionsProviderAction: keyof Omit<ProviderOptionsAppState, 'devices'>
|
||||||
|
) {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.providerOptions[selectOptionsProviderAction],
|
||||||
|
(options) => {
|
||||||
|
if (!options) {
|
||||||
|
return {
|
||||||
|
isFetching: false,
|
||||||
|
values: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFetching: options.isFetching,
|
||||||
|
values: getSelectOptions(options.items),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProviderOptionSelectInputProps
|
||||||
|
extends Omit<
|
||||||
|
EnhancedSelectInputProps<EnhancedSelectInputValue<unknown>, unknown>,
|
||||||
|
'values'
|
||||||
|
> {
|
||||||
|
provider: string;
|
||||||
|
providerData: ProviderOptions;
|
||||||
|
name: string;
|
||||||
|
value: unknown;
|
||||||
|
selectOptionsProviderAction: keyof Omit<ProviderOptionsAppState, 'devices'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProviderOptionSelectInput({
|
||||||
|
provider,
|
||||||
|
providerData,
|
||||||
|
selectOptionsProviderAction,
|
||||||
|
...otherProps
|
||||||
|
}: ProviderOptionSelectInputProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [isRefetchRequired, setIsRefetchRequired] = useState(false);
|
||||||
|
const previousProviderData = usePrevious(providerData);
|
||||||
|
const { isFetching, values } = useSelector(
|
||||||
|
createProviderOptionsSelector(selectOptionsProviderAction)
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOpen = useCallback(() => {
|
||||||
|
if (isRefetchRequired && selectOptionsProviderAction) {
|
||||||
|
setIsRefetchRequired(false);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
fetchOptions({
|
||||||
|
section: selectOptionsProviderAction,
|
||||||
|
action: selectOptionsProviderAction,
|
||||||
|
provider,
|
||||||
|
providerData,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
isRefetchRequired,
|
||||||
|
provider,
|
||||||
|
providerData,
|
||||||
|
selectOptionsProviderAction,
|
||||||
|
dispatch,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectOptionsProviderAction) {
|
||||||
|
dispatch(
|
||||||
|
fetchOptions({
|
||||||
|
section: selectOptionsProviderAction,
|
||||||
|
action: selectOptionsProviderAction,
|
||||||
|
provider,
|
||||||
|
providerData,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [selectOptionsProviderAction, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!previousProviderData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevKey = getProviderDataKey(previousProviderData);
|
||||||
|
const nextKey = getProviderDataKey(providerData);
|
||||||
|
|
||||||
|
if (!isEqual(prevKey, nextKey)) {
|
||||||
|
setIsRefetchRequired(true);
|
||||||
|
}
|
||||||
|
}, [providerData, previousProviderData, setIsRefetchRequired]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (selectOptionsProviderAction) {
|
||||||
|
dispatch(clearOptions({ section: selectOptionsProviderAction }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EnhancedSelectInput
|
||||||
|
{...otherProps}
|
||||||
|
isFetching={isFetching}
|
||||||
|
values={values}
|
||||||
|
onOpen={handleOpen}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProviderOptionSelectInput;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user