diff --git a/src/app/components/elements/DraggableListViewContainer.tsx b/src/app/components/elements/DraggableListViewContainer.tsx new file mode 100644 index 0000000000..35ee2802af --- /dev/null +++ b/src/app/components/elements/DraggableListViewContainer.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { DragDropContext, Droppable, DropResult } from "@hello-pangea/dnd"; + +interface DraggableListViewContainerProps extends React.HTMLAttributes { + reorder: (result: DropResult) => void; +} + +const DraggableListViewContainer = ({ children, reorder, ...rest }: DraggableListViewContainerProps) => { + return + + {(providedDrop) => { + return
+ {children} + {providedDrop.placeholder} +
; + }} +
+
; +}; + +export default DraggableListViewContainer; diff --git a/src/app/components/elements/DraggableListViewItemWrapper.tsx b/src/app/components/elements/DraggableListViewItemWrapper.tsx new file mode 100644 index 0000000000..d149f80029 --- /dev/null +++ b/src/app/components/elements/DraggableListViewItemWrapper.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { Draggable } from "@hello-pangea/dnd"; +import classNames from "classnames"; +import { isAda } from "../../services"; + +interface DraggableListViewWrapperProps extends React.HTMLAttributes { + id: string; + index: number; +} + +const DraggableListViewWrapper = ({ id, index, className, children, ...rest }: DraggableListViewWrapperProps) => { + return + {(providedDrag, snapshot) => { + return
  • + {children} +
  • ; + }} +
    ; +}; + +export default DraggableListViewWrapper; diff --git a/src/app/components/elements/GameboardBuilderRow.tsx b/src/app/components/elements/GameboardBuilderTableRow.tsx similarity index 57% rename from src/app/components/elements/GameboardBuilderRow.tsx rename to src/app/components/elements/GameboardBuilderTableRow.tsx index e260becabf..6fc2513bac 100644 --- a/src/app/components/elements/GameboardBuilderRow.tsx +++ b/src/app/components/elements/GameboardBuilderTableRow.tsx @@ -10,14 +10,11 @@ import { stageLabelMap, TAG_ID, TAG_LEVEL, - isPhy + isPhy, + GameboardBuilderRowInterface, + handleBuilderRowChange } from "../../services"; import React from "react"; -import {AudienceContext} from "../../../IsaacApiTypes"; -import {closeActiveModal, openActiveModal, useAppDispatch} from "../../state"; -import {DraggableProvided, DraggableStateSnapshot} from "@hello-pangea/dnd"; -import {Question} from "../pages/Question"; -import {ContentSummary, GameboardBuilderQuestions, GameboardBuilderQuestionsStackProps} from "../../../IsaacAppTypes"; import {DifficultyIcons} from "./svg/DifficultyIcons"; import classNames from "classnames"; import { Spacer } from "./Spacer"; @@ -27,89 +24,46 @@ import { Markup } from "./markup"; import { ContentPropertyTags } from "./ContentPropertyTags"; import { IconButton } from "./AffixButton"; import { CrossTopicQuestionIndicator } from "./CrossTopicQuestionIndicator"; +import { PreviewQuestionButton } from "./PreviewButton"; -interface GameboardBuilderRowInterface { - provided?: DraggableProvided; - snapshot?: DraggableStateSnapshot; - question: ContentSummary; - currentQuestions: GameboardBuilderQuestions; - undoStack: GameboardBuilderQuestionsStackProps; - redoStack: GameboardBuilderQuestionsStackProps; - creationContext?: AudienceContext; -} - -const GameboardBuilderRow = ( - {provided, snapshot: _snapshot, question, undoStack, currentQuestions, redoStack, creationContext}: GameboardBuilderRowInterface +const GameboardBuilderTableRow = ( + {isDnd, snapshot: _snapshot, question, undoStack, currentQuestions, redoStack, creationContext}: GameboardBuilderRowInterface ) => { - const dispatch = useAppDispatch(); - const tagIcon = (tag: string) => { return {tag}; }; - const openQuestionModal = (urlQuestionId: string) => { - dispatch(openActiveModal({ - closeAction: () => {dispatch(closeActiveModal());}, size: "xl", - title: "Question preview", body: - })); - }; - const audienceViews = determineAudienceViews(question.audience, creationContext); const filteredAudienceViews = audienceViews.length === 0 ? [{stage: undefined, difficulty: undefined}] : filterAudienceViewsByProperties(audienceViews, AUDIENCE_DISPLAY_FIELDS); const cellClasses = "text-start align-middle"; const isSelected = question.id !== undefined && currentQuestions.selectedQuestions.has(question.id); - const handleCheckboxChange = () => { - if (question.id) { - const newSelectedQuestions = new Map(currentQuestions.selectedQuestions); - const newQuestionOrder = [...currentQuestions.questionOrder]; - if (newSelectedQuestions.has(question.id)) { - newSelectedQuestions.delete(question.id); - newQuestionOrder.splice(newQuestionOrder.indexOf(question.id), 1); - } else { - newSelectedQuestions.set(question.id, {...question, creationContext}); - newQuestionOrder.push(question.id); - } - currentQuestions.setSelectedQuestions(newSelectedQuestions); - currentQuestions.setQuestionOrder(newQuestionOrder); - if (provided) { - undoStack.push({questionOrder: currentQuestions.questionOrder, selectedQuestions: currentQuestions.selectedQuestions}); - redoStack.clear(); - } - } - }; - return filteredAudienceViews.map((view, i, arr) => {i === 0 && <>
    - {isAda && provided - ? + {isAda && isDnd + ? handleBuilderRowChange({ isDnd, question, currentQuestions, undoStack, redoStack, creationContext })}/> : handleBuilderRowChange({ isDnd, question, currentQuestions, undoStack, redoStack, creationContext })} />}
    - {provided && Drag to reorder} + {isDnd && Drag to reorder}
    - + {generateQuestionTitle(question)} - +
    @@ -143,4 +97,4 @@ const GameboardBuilderRow = ( ); }; -export default GameboardBuilderRow; +export default GameboardBuilderTableRow; diff --git a/src/app/components/elements/PreviewButton.tsx b/src/app/components/elements/PreviewButton.tsx new file mode 100644 index 0000000000..37a8356545 --- /dev/null +++ b/src/app/components/elements/PreviewButton.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import { closeActiveModal, openActiveModal, useAppDispatch } from "../../state"; +import { Question } from "../pages/Question"; + +export const PreviewQuestionButton = ({id}: {id?: string}) => { + const dispatch = useAppDispatch(); + const openQuestionModal = (urlQuestionId: string) => { + dispatch(openActiveModal({ + closeAction: () => {dispatch(closeActiveModal());}, size: "xl", + title: "Question preview", body: + })); + }; + + if (!id) return null; + + return ; +}; diff --git a/src/app/components/elements/list-groups/AbstractListViewItem.tsx b/src/app/components/elements/list-groups/AbstractListViewItem.tsx index 0dd3716fd9..62fe2bce8e 100644 --- a/src/app/components/elements/list-groups/AbstractListViewItem.tsx +++ b/src/app/components/elements/list-groups/AbstractListViewItem.tsx @@ -18,6 +18,7 @@ import { CrossTopicQuestionIndicator } from "../CrossTopicQuestionIndicator"; import { SupersededDeprecatedBoardContentWarning } from "../../navigation/SupersededDeprecatedWarning"; import { useBookmarks } from "../../../services/bookmarks"; import { FeatureFlag, useFeatureFlag } from "../../../services/featureFlag"; +import { PreviewQuestionButton } from "../PreviewButton"; const Breadcrumb = ({breadcrumb}: {breadcrumb: string[]}) => { return <> @@ -137,6 +138,7 @@ type ALVIType = { hasCaret?: boolean; linkTags?: ListViewTagProps[]; allowBookmarking?: boolean; // if set, displays a bookmark for logged-in users that will save the alvi to the user's bookmarks on click + disableRedirect?: boolean; // the URL is required for bookmarks to show; if we do not want the item to redirect on click, enable } | { // quizzes – have exclusive "preview" and "view test" buttons alviType: "quiz"; @@ -154,6 +156,9 @@ type ALVIType = { deprecated?: boolean; supersededByPath?: string; audienceViews?: ViewingContext[]; + allowBookmarking?: boolean; // as in item type + disableRedirect?: boolean; // as in item type + questionPreviewId?: string; // shows a preview button next to the title; preview opens in modal }; type ALVILayout = { @@ -200,14 +205,14 @@ export const AbstractListViewItem = ({title, icon, subject, subtitle, breadcrumb const isCrossTopic = isAda && tags?.includes("cross_topic"); const isLLM = tags?.includes("llm_question_page"); - const flatLayout = style === "flat" && above['md'](deviceSize); + const flatLayout = style === "flat" && above['lg'](deviceSize); const stackedLayout = style === "stacked" || below["sm"](deviceSize); const wrapTitleTags = below["xs"](deviceSize); const cardBody = <>
    -
    +
    {icon &&
    {icon.label && isAda && above['sm'](deviceSize) &&
    {icon.label}
    } @@ -219,19 +224,22 @@ export const AbstractListViewItem = ({title, icon, subject, subtitle, breadcrumb
    - {url && !isDisabled - ? (url.startsWith("http") - ? + <> + {url && !isDisabled && !("disableRedirect" in typedProps && typedProps.disableRedirect) + ? (url.startsWith("http") + ? + {title} + + : + {title} + + ) + : {title} - - : - {title} - - ) - : - {title} - - } + + } + {isBuilder && typedProps.questionPreviewId && } + {isItem && <> {typedProps.quizTag && {typedProps.quizTag}} }
    - {subtitle &&
    - {subtitle} -
    } - {breadcrumb && !(flatLayout && subtitle) && - - } + {!flatLayout && <> + {subtitle &&
    + {subtitle} +
    } + {breadcrumb && + + } + } {(isItem || isBuilder) && stackedLayout && typedProps.audienceViews &&
    } @@ -272,7 +282,16 @@ export const AbstractListViewItem = ({title, icon, subject, subtitle, breadcrumb {!stackedLayout && <> {isPhy && isItem && typedProps.status && typedProps.status !== CompletionState.ALL_CORRECT && } - {(isItem || isBuilder) && typedProps.audienceViews &&
    0})}> + {flatLayout && (subtitle || breadcrumb) &&
    + {subtitle &&
    + {subtitle} +
    } + {/* additional `&& !subtitle` as compared to stacked, as flat layout only has room for one */} + {breadcrumb && !subtitle && + + } +
    } + {(isItem || isBuilder) && typedProps.audienceViews &&
    0})}>
    } {isGameboard && isTeacherOrAbove(user) && @@ -281,10 +300,16 @@ export const AbstractListViewItem = ({title, icon, subject, subtitle, breadcrumb {isQuiz && } - {isItem && contentId && typedProps.allowBookmarking && isLoggedIn(user) && bookmarksFeatureFlag &&
    + } } {isItem && typedProps.hasCaret &&