Skip to content

Commit df63a8d

Browse files
committed
feat: better uploader control
1 parent bdd9efd commit df63a8d

File tree

2 files changed

+73
-34
lines changed

2 files changed

+73
-34
lines changed

src/components/uploader/OperationUploader.tsx

Lines changed: 68 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ import {
55
FormGroup,
66
H4,
77
Icon,
8+
Spinner,
9+
SpinnerSize,
810
Tag,
911
} from '@blueprintjs/core'
1012
import { Tooltip2 } from '@blueprintjs/popover2'
1113

1214
import { useLevels } from 'apis/arknights'
1315
import { requestOperationUpload } from 'apis/copilotOperation'
1416
import { ComponentType, useState } from 'react'
17+
import { useList } from 'react-use'
1518

1619
import { withSuspensable } from 'components/Suspensable'
1720
import { AppToaster } from 'components/Toaster'
@@ -29,18 +32,23 @@ interface FileEntry {
2932
}
3033

3134
export const OperationUploader: ComponentType = withSuspensable(() => {
32-
const [files, setFiles] = useState(null as FileEntry[] | null)
35+
const [files, { set: setFiles, update: updateFileWhere }] =
36+
useList<FileEntry>([])
37+
3338
const [globalErrors, setGlobalErrors] = useState(null as string[] | null)
39+
const [isProcessing, setIsProcessing] = useState(false)
3440
const [isUploading, setIsUploading] = useState(false)
3541

42+
// reasons are in the order of keys
3643
const nonUploadableReason = Object.entries({
3744
['正在上传,请等待']: isUploading,
38-
['存在错误,请排查问题']: globalErrors?.length,
39-
['请选择文件']: !files?.length,
40-
['文件列表中包含已上传的文件,请重新选择']: files?.some(
45+
['正在解析文件,请等待']: isProcessing,
46+
['请选择文件']: !files.length,
47+
['文件列表中包含已上传的文件,请重新选择']: files.some(
4148
(file) => file.uploaded,
4249
),
43-
['文件存在错误,请修复']: files?.some((file) => file.error),
50+
['文件存在错误,请修改内容']: files.some((file) => file.error),
51+
['存在错误,请排查问题']: globalErrors?.length,
4452
}).find(([, value]) => value)?.[0]
4553

4654
const isUploadable = !nonUploadableReason
@@ -55,16 +63,17 @@ export const OperationUploader: ComponentType = withSuspensable(() => {
5563
setGlobalErrors([levelError.message])
5664
}
5765

58-
const handleFileUpload = async (event: React.FormEvent<HTMLInputElement>) => {
66+
const handleFileSelect = async (event: React.FormEvent<HTMLInputElement>) => {
5967
setGlobalErrors(null)
6068

6169
if (event.currentTarget.files?.length) {
70+
setIsProcessing(true)
71+
6272
const toFileEntry = async (file: File): Promise<FileEntry> => {
6373
const entry: FileEntry = { file }
64-
let content: object
6574

6675
try {
67-
content = await parseOperationFile(file)
76+
let content = await parseOperationFile(file)
6877
content = patchOperation(content, levels)
6978

7079
validateOperation(content)
@@ -79,34 +88,44 @@ export const OperationUploader: ComponentType = withSuspensable(() => {
7988
}
8089

8190
setFiles(
82-
await Promise.all(
83-
Array.from(event.currentTarget.files).map(toFileEntry),
84-
),
91+
await Promise.all(Array.from(event.currentTarget.files, toFileEntry)),
8592
)
93+
94+
setIsProcessing(false)
8695
} else {
87-
setFiles(null)
96+
setFiles([])
8897
}
8998
}
9099

91100
const handleOperationSubmit = async () => {
92-
if (!isUploadable || !files?.length) {
101+
if (!isUploadable || !files.length) {
93102
return
94103
}
95104

96105
setIsUploading(true)
97106
try {
107+
let successCount = 0
108+
98109
await Promise.allSettled(
99110
files.map((file) =>
100111
requestOperationUpload(JSON.stringify(file.operation))
101-
.then(() => (file.uploaded = true))
112+
.then(() => {
113+
successCount++
114+
updateFileWhere((candidate) => candidate === file, {
115+
...file,
116+
uploaded: true,
117+
})
118+
})
102119
.catch((e) => {
103120
console.warn(e)
104-
file.error = `上传失败:${formatError(e)}`
121+
updateFileWhere((candidate) => candidate === file, {
122+
...file,
123+
error: `上传失败:${formatError(e)}`,
124+
})
105125
}),
106126
),
107127
)
108128

109-
const successCount = files.filter((file) => !file.error).length
110129
const errorCount = files.length - successCount
111130

112131
AppToaster.show({
@@ -147,33 +166,51 @@ export const OperationUploader: ComponentType = withSuspensable(() => {
147166
<FileInput
148167
large
149168
fill
169+
disabled={isUploading || isProcessing}
150170
buttonText="浏览"
151-
text={files?.length ? `${files.length} 个文件` : '选择文件...'}
171+
text={files.length ? `${files.length} 个文件` : '选择文件...'}
152172
inputProps={{
153173
accept: '.json',
154174
multiple: true,
155175
}}
156-
onInputChange={handleFileUpload}
176+
onInputChange={handleFileSelect}
157177
/>
158178
</FormGroup>
159179

160180
<Tooltip2
161181
fill
162182
className="mt-4"
163183
placement="top"
184+
disabled={!nonUploadableReason}
164185
content={nonUploadableReason}
165186
>
166-
{/* do not use <Button> because its disabled state does not work well with Tooltip */}
167-
<AnchorButton
168-
large
169-
fill
170-
disabled={!isUploadable}
171-
loading={isUploading}
172-
icon="cloud-upload"
173-
onClick={handleOperationSubmit}
174-
>
175-
上传
176-
</AnchorButton>
187+
{(() => {
188+
const settledCount = files.filter(
189+
(file) => file.uploaded || file.error,
190+
).length
191+
192+
return (
193+
// do not use <Button> because its disabled state does not work well with Tooltip
194+
<AnchorButton
195+
large
196+
fill
197+
disabled={!isUploadable}
198+
icon={
199+
isUploading ? (
200+
<Spinner
201+
size={SpinnerSize.SMALL}
202+
value={settledCount / files.length}
203+
/>
204+
) : (
205+
'cloud-upload'
206+
)
207+
}
208+
onClick={handleOperationSubmit}
209+
>
210+
{isUploading ? `${settledCount}/${files.length}` : '上传'}
211+
</AnchorButton>
212+
)
213+
})()}
177214
</Tooltip2>
178215

179216
{globalErrors && (
@@ -184,8 +221,8 @@ export const OperationUploader: ComponentType = withSuspensable(() => {
184221
</Callout>
185222
)}
186223

187-
{!!files?.length && <div className="mt-4 font-bold">文件详情</div>}
188-
{files?.map(({ file, uploaded, error, operation }, index) => (
224+
{!!files.length && <div className="mt-4 font-bold">文件详情</div>}
225+
{files.map(({ file, uploaded, error, operation }, index) => (
189226
<Callout
190227
className="mt-2"
191228
title={file.name}

src/components/uploader/utils.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { isString } from '@sentry/utils'
22

33
import ajvLocalizeZh from 'ajv-i18n/localize/zh'
4-
import { isFinite, isObject } from 'lodash-es'
4+
import { isFinite, isPlainObject } from 'lodash-es'
55

66
import { CopilotDocV1 } from '../../models/copilot.schema'
77
import { copilotSchemaValidator } from '../../models/copilot.schema.validator'
@@ -25,7 +25,7 @@ export async function parseOperationFile(file: File): Promise<object> {
2525

2626
const json = JSON.parse(fileText)
2727

28-
if (!isObject(json)) {
28+
if (!isPlainObject(json)) {
2929
throw new Error('不是有效的对象')
3030
}
3131

@@ -128,7 +128,9 @@ export function validateOperation(
128128
)
129129
ajvLocalizeZh(copilotSchemaValidator.errors)
130130
throw new Error(
131-
copilotSchemaValidator.errorsText(copilotSchemaValidator.errors),
131+
copilotSchemaValidator.errorsText(copilotSchemaValidator.errors, {
132+
separator: ';',
133+
}),
132134
)
133135
}
134136
} catch (e) {

0 commit comments

Comments
 (0)