Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Import Flow to Match Designs #9891

Merged
merged 29 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
288ecb2
File import in progress...
thsparks Feb 24, 2024
5ff28f2
Merge branch 'master' of https://github.com/microsoft/pxt into thspar…
thsparks Feb 26, 2024
58b4c74
Fix build
thsparks Feb 27, 2024
f432c13
Fix setting rubric when no rubric is already set, and switch tabs aut…
thsparks Feb 27, 2024
9565821
Scope common-modal-body styling to the import rubric modal
thsparks Feb 27, 2024
f0afd1c
Consolidate calls to confirm and replace the rubric
thsparks Feb 27, 2024
8654a98
Add non-functional browse button and fix mouse event handling.
thsparks Feb 27, 2024
ca9e2e0
Working browse button, still needs a11y test
thsparks Feb 27, 2024
22a989d
Merge branch 'master' of https://github.com/microsoft/pxt into thspar…
thsparks Feb 27, 2024
e803427
Cleanup
thsparks Feb 27, 2024
5e7bcf3
Confirmation modal
thsparks Feb 27, 2024
2db89f6
Error layout improvements and prettier
thsparks Feb 27, 2024
45fc68f
Fade error in and out. Toast doesn't work because it's hidden behind …
thsparks Feb 27, 2024
9c777bd
Screen reader support for error message.
thsparks Feb 27, 2024
0185f72
More contrast for a11y
thsparks Feb 27, 2024
f486a3a
Themepack var
thsparks Feb 28, 2024
e69e2d8
Remove unnecessary null coalescing
thsparks Feb 28, 2024
b1f7f28
Clarify comment
thsparks Feb 28, 2024
2bfe845
Add calls to stop propagation of drag events
thsparks Feb 28, 2024
72d3161
Merge branch 'master' of https://github.com/microsoft/pxt into thspar…
thsparks Feb 28, 2024
57de3e4
Move ConfirmationModalProps to types
thsparks Feb 28, 2024
b121b77
Rename ConfirmationModalProps to ConfirmationModalOptions
thsparks Feb 28, 2024
43e83fd
Validate rubric criteria exists before trying to iterate over it (thi…
thsparks Feb 28, 2024
d714738
Use toast accent color for confirmation modal button
thsparks Feb 28, 2024
cd5dbcb
Remove small screen adjustments
thsparks Feb 28, 2024
f080ac4
Add await when loading rubric
thsparks Feb 28, 2024
720693f
More renaming
thsparks Feb 28, 2024
e212501
Remove red from continue button
thsparks Feb 28, 2024
cbe0083
Prettier
thsparks Feb 28, 2024
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
2 changes: 2 additions & 0 deletions teachertool/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { loadCatalogAsync } from "./transforms/loadCatalogAsync";
import { loadValidatorPlansAsync } from "./transforms/loadValidatorPlansAsync";
import { tryLoadLastActiveRubricAsync } from "./transforms/tryLoadLastActiveRubricAsync";
import { ImportRubricModal } from "./components/ImportRubricModal";
import { ConfirmationModal } from "./components/ConfirmationModal";

export const App = () => {
const { state, dispatch } = useContext(AppStateContext);
Expand Down Expand Up @@ -57,6 +58,7 @@ export const App = () => {
<MainPanel />
<CatalogModal />
<ImportRubricModal />
<ConfirmationModal />
<Toasts />
</>
);
Expand Down
48 changes: 48 additions & 0 deletions teachertool/src/components/ConfirmationModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useContext, useEffect, useState } from "react";
import { AppStateContext } from "../state/appStateContext";
import { Modal } from "react-common/components/controls/Modal";
import { hideModal } from "../transforms/hideModal";
import { classList } from "react-common/components/util";
import css from "./styling/ConfirmationModal.module.scss";

export interface IProps {}
export const ConfirmationModal: React.FC<IProps> = () => {
const { state: teacherTool } = useContext(AppStateContext);

function handleCancel() {
hideModal();
teacherTool.confirmationOptions?.onCancel?.();
}

function handleContinue() {
hideModal();
teacherTool.confirmationOptions?.onContinue?.();
}

const actions = [
{
label: lf("Cancel"),
className: "secondary",
onClick: handleCancel,
},
{
label: lf("Continue"),
className: classList(
"primary",
teacherTool.confirmationOptions?.cautionLevel === "high" ? css["caution"] : undefined
),
onClick: handleContinue,
},
];

return teacherTool.modal === "confirmation" && teacherTool.confirmationOptions ? (
<Modal
title={teacherTool.confirmationOptions.title}
onClose={handleCancel}
actions={actions}
className={css["confirmation-modal"]}
>
{teacherTool.confirmationOptions.message}
</Modal>
) : null;
};
108 changes: 108 additions & 0 deletions teachertool/src/components/DragAndDropFileSurface.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { classList } from "react-common/components/util";
import { Strings } from "../constants";
import { NoticeLabel } from "./NoticeLabel";
import { useRef, useState } from "react";
import css from "./styling/DragAndDropFileSurface.module.scss";
import { Button } from "react-common/components/controls/Button";

export interface DragAndDropFileSurfaceProps {
onFileDroppedAsync: (file: File) => void;
errorMessage?: string;
}
export const DragAndDropFileSurface: React.FC<DragAndDropFileSurfaceProps> = ({ onFileDroppedAsync, errorMessage }) => {
const [fileIsOverSurface, setFileIsOverSurface] = useState(false);
const [errorKey, setErrorKey] = useState(0);
const fileInputRef = useRef<HTMLInputElement>(null);

function handleDragOver(event: React.DragEvent<HTMLDivElement>) {
// Stop the browser from intercepting the file.
event.stopPropagation();
event.preventDefault();
}

function handleDragEnter(event: React.DragEvent<HTMLDivElement>) {
event.stopPropagation();
event.preventDefault();
setFileIsOverSurface(true);
}

function handleDragLeave(event: React.DragEvent<HTMLDivElement>) {
event.stopPropagation();
event.preventDefault();
setFileIsOverSurface(false);
}

function handleDrop(event: React.DragEvent<HTMLDivElement>) {
event.stopPropagation();
event.preventDefault();

setFileIsOverSurface(false);

const file = event.dataTransfer.files[0];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be done in a follow-up PR, but I think we need some handling for uploading multiple files. When I dragged in 3 JSON files, it's unclear to me which JSON file is going to get used or even if my other rubric files will get stored or something. If we don't want to support the multiple file scenario, I think just have an error or warning saying that we can only accept one rubric file at a time will helpful.

This will need to be done for the browse scenario as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm not sure what uploading multiple files would mean in terms of import, so my inclination would be to disallow it and provide an error message, but we can discuss as a team and follow up in a separate change.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just importing the first file is fine. I'd prefer not to treat this as an error or warning. The fewer alarm states we create, the better. If we can make it "just work", that's good. The user will learn how it behaves through observation.

Copy link
Contributor

@srietkerk srietkerk Feb 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might just be something we want to cover/ask in user testing. My thoughts are that because we have the modal where we tell the user that importing will override their current rubric, especially with the red coloring, we are putting the user in an alarm state, so I think we should give them the most piece of mind that we possibly can. Personally, when I saw the modal, I didn't even read the rubric title in the top bar because most of my focus was on the button and the details outlining what importing is going to do, so, when I dragged multiple rubrics in, I got nervous not knowing which rubric was going to override my current rubric. In my mind, a "just work" scenario if I drag multiple files in would be that all of my rubrics are now on the site and accessible. I know this isn't what we're going for right now, but I still think it's a valid scenario to consider. For now, I think we can take the first file, but I think we should make an issue for supporting uploading multiple rubrics at once.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On that note, I don't think we should use a red/alarm color for the Continue button. The primary button color should be sufficient.

if (file) {
processNewFile(file);
}
}

function handleFileFromBrowse(event: React.ChangeEvent<HTMLInputElement>) {
const file = event.target.files?.[0];
if (file) {
processNewFile(file);
}
}

function processNewFile(file: File) {
// Change errorKey so that the error component resets (notably, resetting animations).
setErrorKey(errorKey + 1);
onFileDroppedAsync(file);
}

/*
We can't use the drag-and-drop-file-surface directly to handle most drop events, because the child elements interfere with them.
To solve this, we add a transparent div (droppable-surface) over everything and use that for most drag-related event handling.
However, we don't want the transparent droppable-surface to intercept pointer events when there is no drag occurring, so
we still use the drag-and-drop-file-surface to detect dragEnter events and only intercept pointer events on the droppable-surface
after that has happened.
*/
return (
<div className={css["drag-and-drop-file-surface"]} onDragEnter={handleDragEnter}>
<div className={css["instruction-container"]}>
<i className={classList("fas fa-file-upload", css["upload-icon"])}></i>
<div className="no-select">{fileIsOverSurface ? Strings.ReleaseToUpload : Strings.DragAndDrop}</div>
srietkerk marked this conversation as resolved.
Show resolved Hide resolved
<div className={css["or-browse-container"]}>
<span className={css["or-container"]}>{lf("or")}</span>

{/* The button triggers a hidden file input to open the file browser */}
<Button
className={classList("link-button", css["browse-button"])}
title={Strings.Browse}
onClick={() => fileInputRef?.current?.click()}
>
{Strings.Browse}
</Button>
<input
type="file"
ref={fileInputRef}
className="hidden"
onChange={handleFileFromBrowse}
aria-label={Strings.SelectRubricFile}
accept=".json"
/>
</div>
</div>

{errorMessage && (
<div className={css["error-label-container"]} key={errorKey} role="alert" title={errorMessage}>
<NoticeLabel severity="error">{errorMessage}</NoticeLabel>
</div>
)}

<div
className={classList(css["droppable-surface"], fileIsOverSurface ? css["dragging"] : undefined)}
onDrop={handleDrop}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
/>
</div>
);
};
86 changes: 14 additions & 72 deletions teachertool/src/components/ImportRubricModal.tsx
Original file line number Diff line number Diff line change
@@ -1,97 +1,39 @@
import { useContext, useEffect, useState } from "react";
import { useContext, useState } from "react";
import { AppStateContext } from "../state/appStateContext";
import { Modal } from "react-common/components/controls/Modal";
import { hideModal } from "../transforms/hideModal";
import css from "./styling/ImportRubricModal.module.scss";
import { getRubricFromFileAsync } from "../transforms/getRubricFromFileAsync";
import { NoticeLabel } from "./NoticeLabel";
import { Rubric } from "../types/rubric";
import { RubricPreview } from "./RubricPreview";
import { setRubric } from "../transforms/setRubric";
import { DragAndDropFileSurface } from "./DragAndDropFileSurface";
import { Strings } from "../constants";
import css from "./styling/ImportRubricModal.module.scss";
import { replaceActiveRubricAsync } from "../transforms/replaceActiveRubricAsync";

export interface IProps {}

export const ImportRubricModal: React.FC<IProps> = () => {
const { state: teacherTool } = useContext(AppStateContext);
const [selectedFile, setSelectedFile] = useState<File | undefined>(undefined);
const [selectedRubric, setSelectedRubric] = useState<Rubric | undefined>(undefined);
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);

useEffect(() => {
async function updatePreview(file: File) {
const parsedRubric = await getRubricFromFileAsync(file, false /* allow partial */);
if (!parsedRubric) {
setErrorMessage(lf("Invalid rubric file."));
} else {
setErrorMessage(undefined);
}
setSelectedRubric(parsedRubric);
}

if (selectedFile) {
updatePreview(selectedFile);
} else {
setSelectedRubric(undefined);
setErrorMessage(undefined);
}
}, [selectedFile]);

function closeModal() {
setSelectedFile(undefined);
setErrorMessage(undefined);
setSelectedRubric(undefined);
hideModal();
}

function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
if (event.target.files && event.target.files.length > 0) {
setSelectedFile(event.target.files[0]);
async function handleFileDroppedAsync(file: File) {
const parsedRubric = await getRubricFromFileAsync(file, false /* allow partial */);
if (!parsedRubric) {
setErrorMessage(Strings.InvalidRubricFile);
} else {
setSelectedFile(undefined);
}
}

function handleImportClicked() {
if (selectedRubric) {
setRubric(selectedRubric);
setErrorMessage(undefined);
closeModal();
replaceActiveRubricAsync(parsedRubric);
}

closeModal();
}

const actions = [
{
label: lf("Cancel"),
className: "secondary",
onClick: closeModal,
},
{
label: lf("Import"),
className: "primary",
onClick: handleImportClicked,
disabled: !selectedRubric,
},
];

return teacherTool.modal === "import-rubric" ? (
<Modal title={lf("Select rubric to import")} actions={actions} onClose={closeModal}>
<Modal title={Strings.ImportRubric} onClose={closeModal} className={css["import-rubric-modal"]}>
<div className={css["import-rubric"]}>
<NoticeLabel severity="warning">
{lf("Warning! Your current rubric will be overwritten by the imported rubric.")}
</NoticeLabel>
{errorMessage && <NoticeLabel severity="error">{errorMessage}</NoticeLabel>}
{selectedRubric && (
<div className={css["rubric-preview-container"]}>
<RubricPreview rubric={selectedRubric} />
</div>
)}
<input
type="file"
tabIndex={0}
autoFocus
aria-label={lf("Select rubric file.")}
onChange={handleFileChange}
/>
<DragAndDropFileSurface onFileDroppedAsync={handleFileDroppedAsync} errorMessage={errorMessage} />
</div>
</Modal>
) : null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.confirmation-modal {
button.caution {
background-color: var(--pxt-toast-accent-error)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
.drag-and-drop-file-surface {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;

background-color: var(--pxt-content-background);
border-radius: 0.5rem;
border: 1px dashed var(--pxt-content-secondary-foreground);

padding: 2rem;
min-height: 30vh;

.instruction-container {
display: flex;
flex-direction: column;
align-items: center;

color: var(--pxt-content-secondary-foreground);
font-size: 2rem;
font-weight: 600;

.upload-icon {
font-size: 3rem;
}

.or-browse-container {
display: flex;
flex-direction: row;
gap: 0.5rem;

.or-container {
font-weight: 400;
}

.browse-button {
font-weight: 600;
font-size: 2rem;
}
}
}

@keyframes fadeInAndOut {
0% { opacity: 0; }
5% { opacity: 1; }
50% { opacity: 1; }
100% { opacity: 0; }
}

.error-label-container {
position: absolute;
bottom: 1rem;
animation: fadeInAndOut 4s forwards;

i {
display: none;
}
}

.droppable-surface {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;

pointer-events: none;
&.dragging {
pointer-events: all;
}
}
}
28 changes: 17 additions & 11 deletions teachertool/src/components/styling/ImportRubricModal.module.scss
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
.import-rubric {
display: flex;
flex-direction: column;
gap: 0.5rem;
.import-rubric-modal {
div[class*="common-modal-body"] {
background-color: var(--pxt-content-background);
}

.import-rubric {
display: flex;
flex-direction: column;
gap: 0.5rem;

.rubric-preview-container {
max-height: 50vh;
overflow-y: auto;
border: 2px solid var(--pxt-content-foreground);
border-radius: 0.3rem;
background-color: var(--pxt-content-background-glass);
.rubric-preview-container {
max-height: 50vh;
overflow-y: auto;
border: 2px solid var(--pxt-content-foreground);
border-radius: 0.3rem;
background-color: var(--pxt-content-background-glass);
}
}
}
}
Loading