Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,7 @@ const DefaultActionsBar = observer(function DefaultActionsBar(props: ButtonsBarP
}, [props.store]);

if (props.store.fileStatus === "rejected") {
return (
<div className={"entry-details-actions"}>
<RetryButton store={props.store} />
</div>
);
return <RejectedActionsBar store={props.store} />;
}

return (
Expand All @@ -84,6 +80,26 @@ const DefaultActionsBar = observer(function DefaultActionsBar(props: ButtonsBarP
);
});

function RejectedActionsBar({ store }: ButtonsBarProps): ReactElement {
const translations = useTranslationsStore();

const onDismiss = useCallback(() => {
store.dismiss();
}, [store]);

return (
<div className={"entry-details-actions"}>
<RetryButton store={store} />
<ActionButton
icon={<span className={"remove-icon"} aria-hidden />}
title={translations.get("removeButtonTextMessage")}
action={onDismiss}
isDisabled={false}
/>
</div>
);
}

function DismissActionsBar({ store }: ButtonsBarProps): ReactElement {
const translations = useTranslationsStore();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,28 @@ interface DropzoneProps {

export const Dropzone = observer(
({ warningMessage, onDrop, maxSize, acceptFileTypes, disabled }: DropzoneProps): ReactElement => {
const { getRootProps, getInputProps, isDragAccept, isDragReject } = useDropzone({
const { getRootProps, getInputProps, isDragActive, isDragAccept, isDragReject } = useDropzone({
onDrop,
maxSize: maxSize || undefined,
accept: acceptFileTypes,
disabled
});

const translations = useTranslationsStore();
const [type, msg] = getMessage(translations, isDragAccept, isDragReject);
const [type, msg] = getMessage(
translations,
isDragActive && isDragAccept,
isDragActive && isDragReject,
warningMessage
);

return (
<Fragment>
<div
className={classNames("dropzone", {
active: type === "active",
disabled,
warning: !!warningMessage || type === "warning"
warning: type === "warning"
})}
{...getRootProps()}
>
Expand All @@ -52,14 +57,18 @@ type MessageType = "active" | "warning" | "idle";
function getMessage(
translations: TranslationsStore,
isDragAccept: boolean,
isDragReject: boolean
isDragReject: boolean,
warningMessage?: string
): [MessageType, string] {
if (isDragAccept) {
return ["active", translations.get("dropzoneAcceptedMessage")];
}
if (isDragReject) {
return ["warning", translations.get("dropzoneRejectedMessage")];
}
if (warningMessage) {
return ["warning", warningMessage];
}

return ["idle", translations.get("dropzoneIdleMessage")];
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export const FileUploaderRoot = observer((props: FileUploaderContainerProps): Re
warningMessage = translations.get("uploadLimitReachedMessage", rootStore.maxTotalFiles.toString());
} else if (rootStore.createActionFailed) {
warningMessage = translations.get("unavailableCreateActionMessage");
} else if (rootStore.files.some(f => f.fileStatus === "validationError")) {
warningMessage = translations.get("dropzoneRejectedMessage");
}

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,8 @@ export class FileUploaderStore {

get sortedFiles(): FileStore[] {
return [...this.files].sort((a, b) => {
const isErrorA = a.fileStatus === "validationError" ? 1 : 0;
const isErrorB = b.fileStatus === "validationError" ? 1 : 0;
const isErrorA = a.fileStatus === "validationError" || a.fileStatus === "rejected" ? 1 : 0;
const isErrorB = b.fileStatus === "validationError" || b.fileStatus === "rejected" ? 1 : 0;
return isErrorA - isErrorB;
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ describe("FileStore.canRetry — reacts to freed slots", () => {
// ─── FileStore.retry ─────────────────────────────────────────────────────────

describe("FileStore.retry", () => {
test("transitions rejected file to queued", () => {
test("transitions rejected file to uploading via queue reaction", () => {
const store = buildStore({
maxFilesPerUpload: dynamic(new Big(3)),
maxFilesPerBatch: unavailableDynamic()
Expand Down Expand Up @@ -1018,3 +1018,33 @@ describe("upload queue — end-to-end", () => {
expect(store.files.filter(f => f.fileStatus === "uploading")).toHaveLength(0);
});
});

describe("FileUploaderStore validationError files tracking", () => {
test("has validationError files after rejected drop", () => {
const store = buildStore();
store.processDrop([], [{ file: makeFile("bad.exe"), errors: [{ code: "file-invalid-type", message: "bad" }] }]);
expect(store.files.some(f => f.fileStatus === "validationError")).toBe(true);
});

test("no validationError files after all are dismissed", () => {
const store = buildStore();
store.processDrop([], [{ file: makeFile("bad.exe"), errors: [{ code: "file-invalid-type", message: "bad" }] }]);
const errorFile = store.files.find(f => f.fileStatus === "validationError")!;
store.dismissFile(errorFile);
expect(store.files.some(f => f.fileStatus === "validationError")).toBe(false);
});

test("validationError files remain when one dismissed but others exist", () => {
const store = buildStore();
store.processDrop(
[],
[
{ file: makeFile("a.exe"), errors: [{ code: "file-invalid-type", message: "bad" }] },
{ file: makeFile("b.exe"), errors: [{ code: "file-invalid-type", message: "bad" }] }
]
);
const firstError = store.files.find(f => f.fileStatus === "validationError")!;
store.dismissFile(firstError);
expect(store.files.some(f => f.fileStatus === "validationError")).toBe(true);
});
});
Loading