feat: Enhance UploadItemsPane with error handling and status icons for file uploads (#2133)

This commit is contained in:
Dmitry Shilov 2025-05-09 12:11:26 +02:00 committed by GitHub
parent 985c744198
commit 0f896f556b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 115 additions and 29 deletions

View File

@ -356,7 +356,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
value="" value=""
> >
<div <div
className="ms-TextField is-required root-110" className="ms-TextField is-required root-116"
> >
<div <div
className="ms-TextField-wrapper" className="ms-TextField-wrapper"
@ -647,7 +647,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
} }
> >
<label <label
className="ms-Label root-121" className="ms-Label root-127"
htmlFor="TextField0" htmlFor="TextField0"
id="TextFieldLabel2" id="TextFieldLabel2"
> >
@ -656,13 +656,13 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
</LabelBase> </LabelBase>
</StyledLabelBase> </StyledLabelBase>
<div <div
className="ms-TextField-fieldGroup fieldGroup-111" className="ms-TextField-fieldGroup fieldGroup-117"
> >
<input <input
aria-invalid={false} aria-invalid={false}
aria-labelledby="TextFieldLabel2" aria-labelledby="TextFieldLabel2"
autoFocus={true} autoFocus={true}
className="ms-TextField-field field-112" className="ms-TextField-field field-118"
id="TextField0" id="TextField0"
name="collectionIdConfirmation" name="collectionIdConfirmation"
onBlur={[Function]} onBlur={[Function]}
@ -2464,7 +2464,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
> >
<button <button
aria-label="Create" aria-label="Create"
className="ms-Button ms-Button--primary root-122" className="ms-Button ms-Button--primary root-128"
data-is-focusable={true} data-is-focusable={true}
data-test="Panel/OkButton" data-test="Panel/OkButton"
id="sidePanelOkButton" id="sidePanelOkButton"
@ -2477,14 +2477,14 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
type="submit" type="submit"
> >
<span <span
className="ms-Button-flexContainer flexContainer-123" className="ms-Button-flexContainer flexContainer-129"
data-automationid="splitbuttonprimary" data-automationid="splitbuttonprimary"
> >
<span <span
className="ms-Button-textContainer textContainer-124" className="ms-Button-textContainer textContainer-130"
> >
<span <span
className="ms-Button-label label-126" className="ms-Button-label label-132"
id="id__5" id="id__5"
key="id__5" key="id__5"
> >

View File

@ -2,9 +2,13 @@ import {
DetailsList, DetailsList,
DetailsListLayoutMode, DetailsListLayoutMode,
DirectionalHint, DirectionalHint,
FontIcon,
IColumn, IColumn,
SelectionMode, SelectionMode,
TooltipHost, TooltipHost,
getTheme,
mergeStyles,
mergeStyleSets,
} from "@fluentui/react"; } from "@fluentui/react";
import { Upload } from "Common/Upload/Upload"; import { Upload } from "Common/Upload/Upload";
import { UploadDetailsRecord } from "Contracts/ViewModels"; import { UploadDetailsRecord } from "Contracts/ViewModels";
@ -14,6 +18,36 @@ import { getErrorMessage } from "../../Tables/Utilities";
import { useSelectedNode } from "../../useSelectedNode"; import { useSelectedNode } from "../../useSelectedNode";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
const theme = getTheme();
const iconClass = mergeStyles({
verticalAlign: "middle",
maxHeight: "16px",
maxWidth: "16px",
});
const classNames = mergeStyleSets({
fileIconHeaderIcon: {
padding: 0,
fontSize: "16px",
},
fileIconCell: {
textAlign: "center",
selectors: {
"&:before": {
content: ".",
display: "inline-block",
verticalAlign: "middle",
height: "100%",
width: "0px",
visibility: "hidden",
},
},
},
error: [{ color: theme.semanticColors.errorIcon }, iconClass],
accept: [{ color: theme.semanticColors.successIcon }, iconClass],
warning: [{ color: theme.semanticColors.warningIcon }, iconClass],
});
export const UploadItemsPane: FunctionComponent = () => { export const UploadItemsPane: FunctionComponent = () => {
const [files, setFiles] = useState<FileList>(); const [files, setFiles] = useState<FileList>();
const [uploadFileData, setUploadFileData] = useState<UploadDetailsRecord[]>([]); const [uploadFileData, setUploadFileData] = useState<UploadDetailsRecord[]>([]);
@ -60,44 +94,94 @@ export const UploadItemsPane: FunctionComponent = () => {
}; };
const columns: IColumn[] = [ const columns: IColumn[] = [
{
key: "icons",
name: "",
fieldName: "",
className: classNames.fileIconCell,
iconClassName: classNames.fileIconHeaderIcon,
isIconOnly: true,
minWidth: 16,
maxWidth: 16,
onRender: (item: UploadDetailsRecord, index: number, column: IColumn) => {
if (item.numFailed) {
const errorList = (
<ul
aria-label={"error list"}
style={{
margin: "5px 0",
paddingLeft: "20px",
listStyleType: "disc", // Explicitly set to use bullets (dots)
}}
>
{item.errors.map((error, i) => (
<li key={i} style={{ display: "list-item" }}>
{error}
</li>
))}
</ul>
);
return (
<TooltipHost
content={errorList}
id={`tooltip-${index}-${column.key}`}
directionalHint={DirectionalHint.bottomAutoEdge}
>
<FontIcon iconName="Error" className={classNames.error} aria-label="error" />
</TooltipHost>
);
} else if (item.numThrottled) {
return <FontIcon iconName="Warning" className={classNames.warning} aria-label="warning" />;
} else {
return <FontIcon iconName="Accept" className={classNames.accept} aria-label="accept" />;
}
},
},
{ {
key: "fileName", key: "fileName",
name: "FILE NAME", name: "FILE NAME",
fieldName: "fileName", fieldName: "fileName",
minWidth: 140, minWidth: 120,
maxWidth: 140, maxWidth: 140,
onRender: (item: UploadDetailsRecord, index: number, column: IColumn) => {
const fieldContent = item.fileName;
return (
<TooltipHost
content={fieldContent}
id={`tooltip-${index}-${column.key}`}
directionalHint={DirectionalHint.bottomAutoEdge}
>
{fieldContent}
</TooltipHost>
);
},
}, },
{ {
key: "status", key: "status",
name: "STATUS", name: "STATUS",
fieldName: "numSucceeded", fieldName: "numSucceeded",
minWidth: 140, minWidth: 120,
maxWidth: 140, maxWidth: 140,
isRowHeader: true, isRowHeader: true,
isResizable: true, isResizable: true,
data: "string", data: "string",
isPadded: true, isPadded: true,
onRender: (item: UploadDetailsRecord, index: number, column: IColumn) => {
const fieldContent = `${item.numSucceeded} created, ${item.numThrottled} throttled, ${item.numFailed} errors`;
return (
<TooltipHost
content={fieldContent}
id={`tooltip-${index}-${column.key}`}
directionalHint={DirectionalHint.bottomAutoEdge}
>
{fieldContent}
</TooltipHost>
);
},
}, },
]; ];
const _renderItemColumn = (item: UploadDetailsRecord, index: number, column: IColumn) => {
let fieldContent: string;
const tooltipId = `tooltip-${index}-${column.key}`;
switch (column.key) {
case "status":
fieldContent = `${item.numSucceeded} created, ${item.numThrottled} throttled, ${item.numFailed} errors`;
break;
default:
fieldContent = item.fileName;
}
return (
<TooltipHost content={fieldContent} id={tooltipId} directionalHint={DirectionalHint.rightCenter}>
{fieldContent}
</TooltipHost>
);
};
return ( return (
<RightPaneForm {...props}> <RightPaneForm {...props}>
<div className="paneMainContent"> <div className="paneMainContent">
@ -115,7 +199,6 @@ export const UploadItemsPane: FunctionComponent = () => {
<DetailsList <DetailsList
items={uploadFileData} items={uploadFileData}
columns={columns} columns={columns}
onRenderItemColumn={_renderItemColumn}
selectionMode={SelectionMode.none} selectionMode={SelectionMode.none}
layoutMode={DetailsListLayoutMode.justified} layoutMode={DetailsListLayoutMode.justified}
isHeaderVisible={true} isHeaderVisible={true}

View File

@ -1122,6 +1122,9 @@ export default class Collection implements ViewModels.Collection {
stats.numSucceeded++; stats.numSucceeded++;
} else if (response.statusCode === 429) { } else if (response.statusCode === 429) {
documentsToAttempt.push(attemptedDocuments[index]); documentsToAttempt.push(attemptedDocuments[index]);
} else if (response.statusCode === 409) {
stats.numFailed++;
stats.errors.push(`Document with id ${attemptedDocuments[index].id} already exists.`);
} else { } else {
stats.numFailed++; stats.numFailed++;
stats.errors.push(JSON.stringify(response.resourceBody)); stats.errors.push(JSON.stringify(response.resourceBody));