Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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: 1 addition & 1 deletion apps/api/src/query/classification.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Prisma } from "@prisma/client";
import { prisma } from "../model";
import {
CategoryGroup,
ClassificationCategoryTree,
ContentClassification,
PartialContentClassification,
Expand All @@ -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({
Expand Down
13 changes: 0 additions & 13 deletions apps/api/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 2 additions & 5 deletions apps/app/src/drawers/ExploreFilterDrawer.cy.tsx
Original file line number Diff line number Diff line change
@@ -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[] = [
Expand Down
7 changes: 2 additions & 5 deletions apps/app/src/drawers/ExploreFilterDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
8 changes: 2 additions & 6 deletions apps/app/src/paths/Explore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
16 changes: 7 additions & 9 deletions apps/app/src/paths/editor/EditorHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
Expand Down Expand Up @@ -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[],
});
Expand Down
3 changes: 1 addition & 2 deletions apps/app/src/paths/editor/EditorSettingsMode.cy.tsx
Original file line number Diff line number Diff line change
@@ -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"] }, () => {
Expand Down
3 changes: 1 addition & 2 deletions apps/app/src/paths/editor/EditorSettingsMode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
6 changes: 3 additions & 3 deletions apps/app/src/popups/ShareMyContentModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 && (
<Alert status="warning">
<AlertIcon />
<AlertTitle>Not browsable</AlertTitle>
Expand Down
13 changes: 0 additions & 13 deletions apps/app/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
29 changes: 1 addition & 28 deletions apps/app/src/utils/classification.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ReactElement } from "react";
import { Category, CategoryGroup, ContentClassification } from "../types";
import { ContentClassification } from "../types";
import {
Text,
Accordion,
Expand Down Expand Up @@ -73,30 +73,3 @@ export function returnClassificationsAccordionPanel(
</AccordionPanel>
);
}

/**
* 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;
}
3 changes: 2 additions & 1 deletion apps/app/src/widgets/FilterPanel.cy.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof FilterPanel>;

Expand Down
7 changes: 2 additions & 5 deletions apps/app/src/widgets/FilterPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/widgets/editor/EditCategories.cy.tsx
Original file line number Diff line number Diff line change
@@ -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[] = [
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/widgets/editor/EditCategories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/index.ts
Original file line number Diff line number Diff line change
@@ -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";
38 changes: 38 additions & 0 deletions packages/shared/src/logic/browsable.ts
Original file line number Diff line number Diff line change
@@ -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({
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

isActivityFullyCategorized is implemented here but not exported, so it can’t actually be reused by other packages even though the PR description says this logic was moved to shared. Either export/re-export isActivityFullyCategorized (and/or rename isBrowsable to match the exported API), or update the shared API/PR description to reflect that only isBrowsable is intended to be public.

Suggested change
function isActivityFullyCategorized({
export function isActivityFullyCategorized({

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +22
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

isBrowsable currently just proxies the category-completeness check and has TODOs for additional criteria. The name implies broader semantics than it currently enforces, which can mislead callers and makes future implementation of the TODOs a potentially breaking behavioral change. Consider either renaming to something like isActivityFullyCategorized (and exporting that), or fully defining/implementing what “browsable” means in this shared helper.

Suggested change
export function isBrowsable(data: BrowsableData) {
const fullyCategorized = isActivityFullyCategorized(data);
// TODO: check for no errors
// TODO: check for no accessibility violations
return fullyCategorized;
}
/**
* 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({
/**
* @deprecated This helper currently only checks category completeness.
* Prefer `isActivityFullyCategorized` until broader "browsable" criteria are
* fully defined and implemented.
*/
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.
*/
export function isActivityFullyCategorized({

Copilot uses AI. Check for mistakes.
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;
}
12 changes: 12 additions & 0 deletions packages/shared/src/types/categories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export type CategoryGroup = {
name: string;
isRequired: boolean;
isExclusive: boolean;
categories: Category[];
};

export type Category = {
code: string;
description: string;
term: string;
};
Loading