11import { camelCaseObject , getConfig } from '@edx/frontend-platform' ;
22import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth' ;
33import type { TaxonomyData , TaxonomyListData } from './types' ;
4+ import { MAX_TAXONOMY_ITEMS } from './constants' ;
5+ import messages from '../messages' ;
46
57const getApiBaseUrl = ( ) => getConfig ( ) . STUDIO_BASE_URL ;
68const getTaxonomiesV1Endpoint = ( ) => new URL ( 'api/content_tagging/v1/taxonomies/' , getApiBaseUrl ( ) ) . href ;
@@ -53,19 +55,32 @@ export const apiUrls = {
5355 /** Get the URL for a Taxonomy */
5456 taxonomy : ( taxonomyId : number ) => makeUrl ( `${ taxonomyId } /` ) ,
5557 /**
56- * Get the URL for listing the tags of a taxonomy
58+ * Get the URL for listing the tags of a taxonomy.
59+ * The max response size is 10,000 items, as set in the `MAX_TAXONOMY_ITEMS` constant.
60+ * The backend does not support larger responses.
5761 * @param pageIndex Zero-indexed page number
5862 * @param pageSize How many tags per page to load
63+ * @param fullDepth Whether to return max levels of child tags,
64+ * with results limited by the MAX_TAXONOMY_ITEMS constant.
5965 */
60- tagList : ( taxonomyId : number , pageIndex : number , pageSize : number ) => makeUrl ( `${ taxonomyId } /tags/` , {
61- page : ( pageIndex + 1 ) , page_size : pageSize ,
62- } ) ,
66+ tagList : ( taxonomyId : number , {
67+ pageIndex, pageSize, fullDepth, disablePagination,
68+ } : { pageIndex : number | null ; pageSize : number | null ; fullDepth ?: boolean ; disablePagination ?: boolean } ) => {
69+ if ( disablePagination ) {
70+ return makeUrl ( `${ taxonomyId } /tags/` , { full_depth_threshold : fullDepth ? MAX_TAXONOMY_ITEMS : 0 } ) ;
71+ }
72+ return makeUrl ( `${ taxonomyId } /tags/` , {
73+ page : ( pageIndex ?? 0 ) + 1 ,
74+ page_size : pageSize ?? 10 ,
75+ full_depth_threshold : fullDepth ? MAX_TAXONOMY_ITEMS : 0 ,
76+ } ) ;
77+ } ,
6378 /**
6479 * Get _all_ tags below a given parent tag. This may be replaced with something more scalable in the future.
6580 */
6681 allSubtagsOf : ( taxonomyId : number , parentTagValue : string ) => makeUrl ( `${ taxonomyId } /tags/` , {
6782 // Load as deeply as we can
68- full_depth_threshold : 10000 ,
83+ full_depth_threshold : MAX_TAXONOMY_ITEMS ,
6984 parent_tag : parentTagValue ,
7085 } ) ,
7186 /** URL to create a new taxonomy from an import file. */
@@ -74,6 +89,7 @@ export const apiUrls = {
7489 tagsImport : ( taxonomyId ) => makeUrl ( `${ taxonomyId } /tags/import/` ) ,
7590 /** URL to plan (preview what would happen) a taxonomy import */
7691 tagsPlanImport : ( taxonomyId : number ) => makeUrl ( `${ taxonomyId } /tags/import/plan/` ) ,
92+ createTag : ( taxonomyId : number ) => makeUrl ( `${ taxonomyId } /tags/` ) ,
7793} satisfies Record < string , ( ...args : any [ ] ) => string > ;
7894
7995/**
@@ -109,3 +125,46 @@ export async function getTaxonomy(taxonomyId: number): Promise<TaxonomyData> {
109125export function getTaxonomyExportFile ( taxonomyId : number , format : 'json' | 'csv' ) : void {
110126 window . location . href = apiUrls . exportTaxonomy ( taxonomyId , format ) ;
111127}
128+
129+ /**
130+ * Extracts a human-readable error message from the API response.
131+ *
132+ * While most endpoints return an object (e.g., `{ error: "msg" }`), this specific
133+ * backend call may return a raw array of strings: `["error1", "error2"]`. This function normalizes those
134+ * edge cases by returning the first available error message.
135+ * @param {unknown } err - The caught error object from the API.
136+ * @param {Object } intl - The internationalization object to format default messages.
137+ * @returns {string } The first detected error string or a default message if unparseable.
138+ */
139+ export const getApiErrorMessage = ( err : unknown , intl ?: any ) : string => {
140+ const error = err as { message ?: string ; response ?: { data ?: unknown } } ;
141+ const responseData = error ?. response ?. data ;
142+
143+ // `POST /api/content_tagging/v1/taxonomies/:id/tags/ with a duplicate tag name returns
144+ // `["Tag with value 'abblue' already exists for taxonomy."]` as response body.
145+ if ( Array . isArray ( responseData ) ) {
146+ const firstMessage = responseData . find ( ( item ) : item is string => typeof item === 'string' && item . trim ( ) . length > 0 ) ;
147+ if ( firstMessage ) {
148+ return firstMessage ;
149+ }
150+ }
151+
152+ if ( typeof responseData === 'string' && responseData . trim ( ) . length > 0 ) {
153+ return responseData ;
154+ }
155+
156+ if ( responseData && typeof responseData === 'object' ) {
157+ const objectData = responseData as { error ?: string ; detail ?: string ; message ?: string } ;
158+ if ( typeof objectData . error === 'string' && objectData . error . trim ( ) . length > 0 ) {
159+ return objectData . error ;
160+ }
161+ if ( typeof objectData . message === 'string' && objectData . message . trim ( ) . length > 0 ) {
162+ return objectData . message ;
163+ }
164+ if ( typeof objectData . detail === 'string' && objectData . detail . trim ( ) . length > 0 ) {
165+ return objectData . detail ;
166+ }
167+ }
168+
169+ return error ?. message || ( intl ? intl . formatMessage ( messages . unknownErrorMessage ) : 'Unknown error' ) ;
170+ } ;
0 commit comments