diff --git a/apps/api/src/query/classification.ts b/apps/api/src/query/classification.ts index d2645ea14..3a61a2ef1 100644 --- a/apps/api/src/query/classification.ts +++ b/apps/api/src/query/classification.ts @@ -1,7 +1,6 @@ import { Prisma } from "@prisma/client"; import { prisma } from "../model"; import { - CategoryGroup, ClassificationCategoryTree, ContentClassification, PartialContentClassification, @@ -20,6 +19,7 @@ import { getIsEditor, } from "../utils/permissions"; import { InvalidRequestError } from "../utils/error"; +import { CategoryGroup } from "@doenet-tools/shared"; export async function getClassificationCategories() { const results = await prisma.classificationSystems.findMany({ diff --git a/apps/api/src/types.ts b/apps/api/src/types.ts index fe753475a..21cfc44eb 100644 --- a/apps/api/src/types.ts +++ b/apps/api/src/types.ts @@ -103,19 +103,6 @@ export type UserInfoWithEmail = UserInfo & { isEditor?: boolean; }; -export type CategoryGroup = { - name: string; - isRequired: boolean; - isExclusive: boolean; - categories: Category[]; -}; - -export type Category = { - code: string; - description: string; - term: string; -}; - export type ContentClassification = { id: number; code: string; diff --git a/apps/app/src/drawers/ExploreFilterDrawer.cy.tsx b/apps/app/src/drawers/ExploreFilterDrawer.cy.tsx index c13879c55..f4563cd63 100644 --- a/apps/app/src/drawers/ExploreFilterDrawer.cy.tsx +++ b/apps/app/src/drawers/ExploreFilterDrawer.cy.tsx @@ -1,9 +1,6 @@ import { ExploreFilterDrawer } from "./ExploreFilterDrawer"; -import { - CategoryGroup, - PartialContentClassification, - UserInfo, -} from "../types"; +import { PartialContentClassification, UserInfo } from "../types"; +import { CategoryGroup } from "@doenet-tools/shared"; describe("ExploreFilterDrawer", { tags: ["@group1"] }, () => { const mockAuthors: UserInfo[] = [ diff --git a/apps/app/src/drawers/ExploreFilterDrawer.tsx b/apps/app/src/drawers/ExploreFilterDrawer.tsx index dd7eed32a..ea304d980 100644 --- a/apps/app/src/drawers/ExploreFilterDrawer.tsx +++ b/apps/app/src/drawers/ExploreFilterDrawer.tsx @@ -6,11 +6,8 @@ import { DrawerContent, DrawerOverlay, } from "@chakra-ui/react"; -import { - CategoryGroup, - PartialContentClassification, - UserInfo, -} from "../types"; +import { PartialContentClassification, UserInfo } from "../types"; +import { CategoryGroup } from "@doenet-tools/shared"; import { NavigateFunction } from "react-router"; import { FilterPanel } from "../widgets/FilterPanel"; diff --git a/apps/app/src/paths/Explore.tsx b/apps/app/src/paths/Explore.tsx index 6cdb44c5b..15982d188 100644 --- a/apps/app/src/paths/Explore.tsx +++ b/apps/app/src/paths/Explore.tsx @@ -36,12 +36,8 @@ import Searchbar from "../widgets/SearchBar"; import { Form, useFetcher } from "react-router"; import { CardContent } from "../widgets/Card"; import { createNameNoTag, createNameCheckCurateTag } from "../utils/names"; -import { - Content, - UserInfo, - PartialContentClassification, - CategoryGroup, -} from "../types"; +import { Content, UserInfo, PartialContentClassification } from "../types"; +import { CategoryGroup } from "@doenet-tools/shared"; import CardList from "../widgets/CardList"; import { intWithCommas } from "../utils/formatting"; import { MdFilterAlt, MdFilterAltOff } from "react-icons/md"; diff --git a/apps/app/src/paths/editor/EditorHeader.tsx b/apps/app/src/paths/editor/EditorHeader.tsx index c2d3f4b95..bdb5c0847 100644 --- a/apps/app/src/paths/editor/EditorHeader.tsx +++ b/apps/app/src/paths/editor/EditorHeader.tsx @@ -44,13 +44,7 @@ import { IoGitBranch } from "react-icons/io5"; import { LuCircleHelp, LuLibraryBig } from "react-icons/lu"; import axios from "axios"; -import { - AssignmentStatus, - ContentType, - ContentDescription, - CategoryGroup, - Category, -} from "../../types"; +import { AssignmentStatus, ContentType, ContentDescription } from "../../types"; import { contentTypeToName, getIconInfo } from "../../utils/activity"; import { SiteContext } from "../SiteHeader"; import { getDiscourseUrl } from "../../utils/discourse"; @@ -63,12 +57,16 @@ import { LibraryEditorControls } from "../../widgets/editor/LibraryEditorControl import { editorUrl } from "../../utils/url"; import { NameBar } from "../../widgets/NameBar"; import { loader as settingsLoader } from "./EditorSettingsMode"; -import { isActivityFullyCategorized } from "../../utils/classification"; import { useIframeMenuDismissOverlay } from "../../utils/useIframeMenuDismissOverlay"; import { MenuDismissOverlay } from "../../components/MenuDismissOverlay"; import { IFRAME_MENU_IDS } from "../../utils/iframeMenuIds"; import { useControlledMenu } from "../../utils/useControlledMenu"; import { useMenuTooltipSuppression } from "../../utils/useMenuTooltipSuppression"; +import { + isBrowsable, + type Category, + type CategoryGroup, +} from "@doenet-tools/shared"; export async function loader({ params, @@ -165,7 +163,7 @@ export function EditorHeader() { const notBrowsable = isPublic && categoryCheckFetcher.data && - !isActivityFullyCategorized({ + !isBrowsable({ allCategories: categoryCheckFetcher.data.allCategories as CategoryGroup[], categories: categoryCheckFetcher.data.categories as Category[], }); diff --git a/apps/app/src/paths/editor/EditorSettingsMode.cy.tsx b/apps/app/src/paths/editor/EditorSettingsMode.cy.tsx index 609d43e39..738d44a36 100644 --- a/apps/app/src/paths/editor/EditorSettingsMode.cy.tsx +++ b/apps/app/src/paths/editor/EditorSettingsMode.cy.tsx @@ -1,12 +1,11 @@ import { EditorSettingsModeComponent } from "./EditorSettingsMode"; import { AssignmentMode, - Category, - CategoryGroup, ContentClassification, DoenetmlVersion, LicenseCode, } from "../../types"; +import { Category, CategoryGroup } from "@doenet-tools/shared"; import { FetcherWithComponents } from "react-router"; describe("EditorSettingsModeComponent", { tags: ["@group1"] }, () => { diff --git a/apps/app/src/paths/editor/EditorSettingsMode.tsx b/apps/app/src/paths/editor/EditorSettingsMode.tsx index 5778f11a9..fafcd14c3 100644 --- a/apps/app/src/paths/editor/EditorSettingsMode.tsx +++ b/apps/app/src/paths/editor/EditorSettingsMode.tsx @@ -7,11 +7,10 @@ import { import { AssignmentMode, ContentClassification, - Category, DoenetmlVersion, LicenseCode, - CategoryGroup, } from "../../types"; +import { Category, CategoryGroup } from "@doenet-tools/shared"; import axios from "axios"; import { Box, Heading, VStack } from "@chakra-ui/react"; import { BlueBanner } from "../../widgets/BlueBanner"; diff --git a/apps/app/src/popups/ShareMyContentModal.tsx b/apps/app/src/popups/ShareMyContentModal.tsx index c1329d0d4..7edd2ad3c 100644 --- a/apps/app/src/popups/ShareMyContentModal.tsx +++ b/apps/app/src/popups/ShareMyContentModal.tsx @@ -34,7 +34,7 @@ import { FiCode } from "react-icons/fi"; import { loader as settingsLoader } from "../paths/editor/EditorSettingsMode"; import { editorUrl } from "../utils/url"; -import { isActivityFullyCategorized } from "../utils/classification"; +import { isBrowsable } from "@doenet-tools/shared"; export async function loadShareStatus({ params }: { params: any }) { const { data } = await axios.get( @@ -238,12 +238,12 @@ function SharePublicly({ const [copiedShareLink, setCopiedShareLink] = useState(false); const [copiedEmbedCode, setCopiedEmbedCode] = useState(false); - const unspecifiedCategories = !isActivityFullyCategorized({ + const notBrowsable = !isBrowsable({ allCategories: settings.allCategories, categories: settings.categories, }); - const browseWarning = unspecifiedCategories && ( + const browseWarning = notBrowsable && ( Not browsable diff --git a/apps/app/src/types.ts b/apps/app/src/types.ts index 6dee1f845..b89274a15 100644 --- a/apps/app/src/types.ts +++ b/apps/app/src/types.ts @@ -87,19 +87,6 @@ export type UserInfoWithEmail = UserInfo & { isEditor?: boolean; }; -export type CategoryGroup = { - name: string; - isRequired: boolean; - isExclusive: boolean; - categories: Category[]; -}; - -export type Category = { - code: string; - description: string; - term: string; -}; - export type ContentClassification = { id: number; code: string; diff --git a/apps/app/src/utils/classification.tsx b/apps/app/src/utils/classification.tsx index 7ea476f47..46016cf96 100644 --- a/apps/app/src/utils/classification.tsx +++ b/apps/app/src/utils/classification.tsx @@ -1,5 +1,5 @@ import { ReactElement } from "react"; -import { Category, CategoryGroup, ContentClassification } from "../types"; +import { ContentClassification } from "../types"; import { Text, Accordion, @@ -73,30 +73,3 @@ export function returnClassificationsAccordionPanel( ); } - -/** - * Detect whether or not this activity has the required categories filled out. - * For each group that is required, make sure this activity has at least 1 category in that group. - * Returns true if all required groups have at least one category filled out, false otherwise. - */ -export function isActivityFullyCategorized({ - allCategories, - categories, -}: { - allCategories: CategoryGroup[]; - categories: Category[]; -}) { - const existingCodes = categories.map((c) => c.code); - - for (const group of allCategories.filter((g) => g.isRequired)) { - const groupCategoryCodes = group.categories.map((c) => c.code); - const intersection = existingCodes.filter((code) => - groupCategoryCodes.includes(code), - ); - if (intersection.length === 0) { - return false; - } - } - - return true; -} diff --git a/apps/app/src/widgets/FilterPanel.cy.tsx b/apps/app/src/widgets/FilterPanel.cy.tsx index b8da2d163..3adc40de2 100644 --- a/apps/app/src/widgets/FilterPanel.cy.tsx +++ b/apps/app/src/widgets/FilterPanel.cy.tsx @@ -1,5 +1,6 @@ import { FilterPanel } from "./FilterPanel"; -import { CategoryGroup, UserInfo } from "../types"; +import { UserInfo } from "../types"; +import { CategoryGroup } from "@doenet-tools/shared"; type FilterPanelProps = React.ComponentProps; diff --git a/apps/app/src/widgets/FilterPanel.tsx b/apps/app/src/widgets/FilterPanel.tsx index 03a783394..11efe303f 100644 --- a/apps/app/src/widgets/FilterPanel.tsx +++ b/apps/app/src/widgets/FilterPanel.tsx @@ -25,11 +25,8 @@ import { ReactElement } from "react"; import { createNameNoTag } from "../utils/names"; import { CloseIcon } from "@chakra-ui/icons"; import { activityCategoryIcons } from "../utils/activity"; -import { - CategoryGroup, - PartialContentClassification, - UserInfo, -} from "../types"; +import { PartialContentClassification, UserInfo } from "../types"; +import { CategoryGroup } from "@doenet-tools/shared"; import { intWithCommas } from "../utils/formatting"; import { Link as ReactRouterLink, NavigateFunction } from "react-router"; import { clearQueryParameter } from "../utils/explore"; diff --git a/apps/app/src/widgets/editor/EditCategories.cy.tsx b/apps/app/src/widgets/editor/EditCategories.cy.tsx index 088797fc8..f76a3d0e2 100644 --- a/apps/app/src/widgets/editor/EditCategories.cy.tsx +++ b/apps/app/src/widgets/editor/EditCategories.cy.tsx @@ -1,5 +1,5 @@ import { EditCategories } from "./EditCategories"; -import { CategoryGroup } from "../../types"; +import { CategoryGroup } from "@doenet-tools/shared"; describe("EditCategories Component", { tags: ["@group3"] }, () => { const mockAllCategories: CategoryGroup[] = [ diff --git a/apps/app/src/widgets/editor/EditCategories.tsx b/apps/app/src/widgets/editor/EditCategories.tsx index ecd9074a3..c5328be61 100644 --- a/apps/app/src/widgets/editor/EditCategories.tsx +++ b/apps/app/src/widgets/editor/EditCategories.tsx @@ -16,8 +16,8 @@ import { } from "@chakra-ui/react"; import { useFetcher } from "react-router"; import { optimistic } from "../../utils/optimistic_ui"; -import { Category, CategoryGroup } from "../../types"; import { activityCategoryIcons } from "../../utils/activity"; +import { Category, CategoryGroup } from "@doenet-tools/shared"; export function EditCategories({ contentId, diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 32bfd3640..2fdedfa54 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,4 +1,6 @@ export * from "./types/activityViewer.js"; +export * from "./types/categories.js"; export * from "./utils/ipfs.js"; export * from "./utils/test.js"; export * from "./apiTypes.js"; +export * from "./logic/browsable.js"; diff --git a/packages/shared/src/logic/browsable.ts b/packages/shared/src/logic/browsable.ts new file mode 100644 index 000000000..62d352c4d --- /dev/null +++ b/packages/shared/src/logic/browsable.ts @@ -0,0 +1,38 @@ +import type { Category, CategoryGroup } from "../types/categories.js"; + +type BrowsableData = { + allCategories: CategoryGroup[]; + categories: Category[]; +}; + +/** + * Determine whether this activity should be discoverable. + * Currently this checks category completeness; additional browsable criteria may be added here later. + * TODO: check for no errors + * TODO: check for no accessibility violations + */ +export function isBrowsable(data: BrowsableData) { + return isActivityFullyCategorized(data); +} + +/** + * Detect whether or not this activity has the required categories filled out. + * For each group that is required, make sure this activity has at least 1 category in that group. + */ +function isActivityFullyCategorized({ + allCategories, + categories, +}: BrowsableData) { + const existingCodes = new Set(categories.map((c) => c.code)); + + for (const group of allCategories.filter((g) => g.isRequired)) { + const hasMatch = group.categories.some((category) => + existingCodes.has(category.code), + ); + if (!hasMatch) { + return false; + } + } + + return true; +} diff --git a/packages/shared/src/types/categories.ts b/packages/shared/src/types/categories.ts new file mode 100644 index 000000000..102afa349 --- /dev/null +++ b/packages/shared/src/types/categories.ts @@ -0,0 +1,12 @@ +export type CategoryGroup = { + name: string; + isRequired: boolean; + isExclusive: boolean; + categories: Category[]; +}; + +export type Category = { + code: string; + description: string; + term: string; +};