From e4032be51f87e0976e357c7c900e21fc30d0390a Mon Sep 17 00:00:00 2001 From: Charles Nykamp Date: Sat, 24 Aug 2024 18:34:03 -0500 Subject: [PATCH] Add classification by browsing --- .../ToolPanels/ClassificationSettings.tsx | 177 ++++++++++++++++-- .../ToolPanels/ContentSettingsDrawer.tsx | 29 ++- .../ToolPanels/GeneralContentControls.tsx | 6 - client/src/_utils/types.ts | 21 +++ server/prisma/seed.ts | 22 ++- server/src/index.ts | 17 ++ server/src/model.test.ts | 5 +- server/src/model.ts | 133 ++++++++++--- server/src/types.ts | 30 ++- 9 files changed, 357 insertions(+), 83 deletions(-) diff --git a/client/src/Tools/_framework/ToolPanels/ClassificationSettings.tsx b/client/src/Tools/_framework/ToolPanels/ClassificationSettings.tsx index 9a042e6e0..c7bde5b13 100644 --- a/client/src/Tools/_framework/ToolPanels/ClassificationSettings.tsx +++ b/client/src/Tools/_framework/ToolPanels/ClassificationSettings.tsx @@ -22,9 +22,17 @@ import { HStack, Tooltip, Spinner, + CardBody, + Card, + CardHeader, + Button, } from "@chakra-ui/react"; import AsyncSelect from "react-select/async"; -import { ContentClassification, ContentStructure } from "../../../_utils/types"; +import { + ClassificationSystemTree, + ContentClassification, + ContentStructure, +} from "../../../_utils/types"; export async function classificationSettingsActions({ formObj, @@ -91,19 +99,23 @@ export function ClassificationSettings({ setClassifyItemRemoveSpinner(0); }, [contentData]); - let [allClassifications, setAllClassifications] = useState(); + let [allClassifications, setAllClassifications] = useState< + ClassificationSystemTree[] + >([]); useEffect(() => { - async function getAllClassifications() { - const { data } = await axios.get( - `/api/getContributorHistory/${contentData.id}`, - ); - } - - if (!contentData.isFolder) { - getAllClassifications(); + async function setClassifications() { + const { data } = await axios.get(`/api/getAllClassificationInfo`); + setAllClassifications(data); } + setClassifications(); }, []); + let [selectedSystem, setSelectedSystem] = useState(null); + let [selectedCategory, setSelectedCategory] = useState(null); + let [selectedSubCategory, setSelectedSubCategory] = useState( + null, + ); + return ( <> @@ -117,7 +129,9 @@ export function ClassificationSettings({ Existing Classifications {contentData.classifications.length === 0 ? ( - None added yet. + + None added yet. + ) : ( {contentData.classifications.map((classification, i) => ( @@ -128,7 +142,10 @@ export function ClassificationSettings({ ;{classification.code} - {classification.subCategory.category.system.name} + { + classification.subCategory.category.system + .name + } @@ -165,13 +182,21 @@ export function ClassificationSettings({ - {classification.subCategory.category.system.categoryLabel}:{" "} + { + classification.subCategory.category.system + .categoryLabel + } + :{" "} {classification.subCategory.category.category} - {classification.subCategory.category.system.subCategoryLabel}:{" "} + { + classification.subCategory.category.system + .subCategoryLabel + } + :{" "} {classification.subCategory.subCategory} @@ -187,9 +212,111 @@ export function ClassificationSettings({ Add a Classification - - - + + + + + + {selectedSubCategory !== null + ? allClassifications[selectedSystem!].categories[ + selectedCategory! + ].subCategories[selectedSubCategory].classifications.map( + (classification) => ( + + + + {classification.code} + + {contentData.classifications + .map((c) => c.id) + .includes(classification.id) ? ( + Already added + ) : ( + + )} + + {classification.description} + + + ), + ) + : null} @@ -228,7 +355,9 @@ export function ClassificationSettings({ > {val.label.code + (val.label.subCategory.category.category - ? " (" + val.label.subCategory.category.category + ")" + ? " (" + + val.label.subCategory.category.category + + ")" : "")} @@ -244,7 +373,11 @@ export function ClassificationSettings({ - {val.label.subCategory.category.system.categoryLabel}: + { + val.label.subCategory.category.system + .categoryLabel + } + : {" "} - {val.label.subCategory.category.system.subCategoryLabel}: + { + val.label.subCategory.category.system + .subCategoryLabel + } + : {" "} setTabIndex(index)}> General - Classifications ({contentData.classifications.length}) + + {!contentData.isFolder ? ( + + Classifications ({contentData.classifications.length}) + + ) : null} {haveSupportingFiles ? ( Support Files ) : null} @@ -132,13 +141,15 @@ export function ContentSettingsDrawer({ allDoenetmlVersions={allDoenetmlVersions} /> - - - + {!contentData.isFolder ? ( + + + + ) : null} {haveSupportingFiles ? ( ([]); - let [classifySelectorInput, setClassifySelectorInput] = useState(""); - const classificationsAlreadyAdded = contentData.classifications.map( - (c2) => c2.id, - ); - // let learningOutcomesInit = activityData.learningOutcomes; // if (learningOutcomesInit == null) { // learningOutcomesInit = [""]; @@ -462,7 +457,6 @@ export function GeneralContentControls({ - ); diff --git a/client/src/_utils/types.ts b/client/src/_utils/types.ts index ee15eb564..c312b469f 100644 --- a/client/src/_utils/types.ts +++ b/client/src/_utils/types.ts @@ -117,3 +117,24 @@ export type DocRemixItem = { activityName: string; owner: UserInfo; }; + + +export type ClassificationSystemTree = { + id: number; + name: string; + categoryLabel: string; + subCategoryLabel: string; + categories: { + id: number; + category: string; + subCategories: { + id: number; + subCategory: string; + classifications: { + id: number; + code: string; + description: string; + }[] + }[] + }[] +}; \ No newline at end of file diff --git a/server/prisma/seed.ts b/server/prisma/seed.ts index 2a1d57649..266c18c96 100644 --- a/server/prisma/seed.ts +++ b/server/prisma/seed.ts @@ -157,6 +157,16 @@ async function main() { "Count to 100 by ones and by tens.", commonCoreCardinalityId, ); + await upsertClassification( + "K.CC.2", + "Count forward beginning from a given number within the known sequence (instead of having to begin at 1).", + commonCoreCardinalityId, + ); + await upsertClassification( + "K.CC.3", + "Write numbers from 0 to 20. Represent a number of objects with a written numeral 0-20 (with 0 representing a count of no objects).", + commonCoreCardinalityId, + ); await upsertClassification( "K.OA.1", "Represent addition and subtraction with objects, fingers, mental images, drawings (Drawings need not show details, but should show the mathematics in the problem.(This applies wherever drawings are mentioned in the Standards.)), sounds (e.g., claps), acting out situations, verbal explanations, expressions, or equations.", @@ -173,14 +183,8 @@ async function main() { "Grade", "Standard", ); - const mnEighthId = await upsertClassificationCategory( - "8", - mnId, - ); - const mnNinthId = await upsertClassificationCategory( - "9", - mnId, - ); + const mnEighthId = await upsertClassificationCategory("8", mnId); + const mnNinthId = await upsertClassificationCategory("9", mnId); const mnFuncId = await upsertClassificationSubCategory( "Understand the concept of function in real-world and mathematical situations, and distinguish between linear and nonlinear functions.", mnEighthId, @@ -200,8 +204,6 @@ async function main() { mnProbabilityId, ); - - await prisma.licenses.upsert({ where: { code: "CCBYSA" }, update: { diff --git a/server/src/index.ts b/server/src/index.ts index 7ee351aeb..50afa65cc 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -79,6 +79,7 @@ import { getDocumentSource, setPreferredFolderView, getPreferredFolderView, + getAllClassificationInfo, } from "./model"; import session from "express-session"; import { PrismaSessionStore } from "@quixo3/prisma-session-store"; @@ -2476,6 +2477,22 @@ app.get( }, ); +app.get( + "/api/getAllClassificationInfo", + async (req: Request, res: Response, next: NextFunction) => { + try { + const results = await getAllClassificationInfo(); + res.send(results); + } catch (e) { + if (e instanceof InvalidRequestError) { + res.status(e.errorCode).send(e.message); + return; + } + next(e); + } + }, +); + app.post( "/api/setPreferredFolderView", async (req: Request, res: Response, next: NextFunction) => { diff --git a/server/src/model.test.ts b/server/src/model.test.ts index d6550455d..f5d4aa8b1 100644 --- a/server/src/model.test.ts +++ b/server/src/model.test.ts @@ -6925,10 +6925,7 @@ test("Content classifications can only be edited by activity owner", async () => const classifications = await getClassifications(activityId, userId); expect(classifications.length).toBe(1); expect(classifications[0]).toHaveProperty("code", "K.CC.1"); - expect(classifications[0]).toHaveProperty( - "id", - classificationId, - ); + expect(classifications[0]).toHaveProperty("id", classificationId); } // Remove diff --git a/server/src/model.ts b/server/src/model.ts index c445f8555..bffe0915b 100644 --- a/server/src/model.ts +++ b/server/src/model.ts @@ -4,6 +4,7 @@ import { DateTime } from "luxon"; import { fromUUID } from "./utils/uuid"; import { AssignmentStatus, + ClassificationSystemTree, ContentClassification, ContentStructure, DocHistory, @@ -951,11 +952,11 @@ export async function getActivityEditorData( category: { include: { system: true, - } - } - } - } - } + }, + }, + }, + }, + }, }, }, }, @@ -1077,11 +1078,11 @@ export async function getActivityEditorData( category: { include: { system: true, - } - } - } - } - } + }, + }, + }, + }, + }, }, }, }, @@ -3621,10 +3622,10 @@ export async function getMyFolderContent({ category: { include: { system: true, - } - } - } - } + }, + }, + }, + }, }, }, }, @@ -3932,11 +3933,11 @@ export async function searchMyFolderContent({ category: { include: { system: true, - } - } - } - } - } + }, + }, + }, + }, + }, }, }, }, @@ -4255,6 +4256,72 @@ export async function getSharedFolderContent({ }; } +export async function getAllClassificationInfo() { + const results = await prisma.classificationSystems.findMany({ + orderBy: { + name: "asc", + }, + select: { + id: true, + name: true, + categoryLabel: true, + subCategoryLabel: true, + classificationCategory: { + orderBy: { + category: "asc", + }, + select: { + id: true, + category: true, + classificationSubCategory: { + orderBy: { + subCategory: "asc", + }, + select: { + id: true, + subCategory: true, + classifications: { + orderBy: { + code: "asc", + }, + select: { + id: true, + code: true, + description: true, + }, + }, + }, + }, + }, + }, + }, + }); + const formattedResults: ClassificationSystemTree[] = results.map((system) => { + return { + id: system.id, + name: system.name, + categoryLabel: system.categoryLabel, + subCategoryLabel: system.subCategoryLabel, + categories: system.classificationCategory.map((category) => { + return { + id: category.id, + category: category.category, + subCategories: category.classificationSubCategory.map( + (subCategory) => { + return { + id: subCategory.id, + subCategory: subCategory.subCategory, + classifications: subCategory.classifications, + }; + }, + ), + }; + }), + }; + }); + return formattedResults; +} + export async function searchPossibleClassifications(query: string) { const query_words = query.split(" "); const results: ContentClassification[] = @@ -4264,9 +4331,15 @@ export async function searchPossibleClassifications(query: string) { OR: [ { code: { contains: query_word } }, { description: { contains: query_word } }, - { subCategory: { subCategory: { contains: query_word }}}, - { subCategory: { category: {category: {contains: query_word }}}}, - { subCategory: {category: {system: {name: {contains: query_word }}}}}, + { subCategory: { subCategory: { contains: query_word } } }, + { + subCategory: { category: { category: { contains: query_word } } }, + }, + { + subCategory: { + category: { system: { name: { contains: query_word } } }, + }, + }, ], })), }, @@ -4276,9 +4349,9 @@ export async function searchPossibleClassifications(query: string) { category: { include: { system: true, - } - } - } + }, + }, + }, }, }, }); @@ -4405,15 +4478,17 @@ export async function getClassifications( category: { include: { system: true, - } - } - } + }, + }, + }, }, }, }, }, }); - const formatted: ContentClassification[] = classifications.map((c) => c.classification); + const formatted: ContentClassification[] = classifications.map( + (c) => c.classification, + ); return formatted; } diff --git a/server/src/types.ts b/server/src/types.ts index 995233f90..5df49d81b 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -31,11 +31,11 @@ export type ContentClassification = { id: number; name: string; categoryLabel: string; - subCategoryLabel: string; - } - } - } -} + subCategoryLabel: string; + }; + }; + }; +}; export type ContentStructure = { id: Buffer; @@ -112,3 +112,23 @@ export type DocHistory = { }; }[]; }; + +export type ClassificationSystemTree = { + id: number; + name: string; + categoryLabel: string; + subCategoryLabel: string; + categories: { + id: number; + category: string; + subCategories: { + id: number; + subCategory: string; + classifications: { + id: number; + code: string; + description: string; + }[] + }[] + }[] +}; \ No newline at end of file