From 588b7be3a178b438e6e46fd47aed720956a186cf Mon Sep 17 00:00:00 2001 From: Jesper Hodge Date: Tue, 14 Apr 2026 12:23:48 -0400 Subject: [PATCH 01/12] fix: taxonomy alignment and error message --- src/messages.ts | 4 + src/taxonomy/data/apiHooks.ts | 29 ++-- src/taxonomy/tag-list/UsageCountDisplay.tsx | 25 +++ src/taxonomy/tag-list/hooks.ts | 36 +++-- src/taxonomy/tag-list/tagColumns.tsx | 26 +-- src/taxonomy/tag-list/types.ts | 9 ++ src/taxonomy/tree-table/CreateRow.test.tsx | 3 +- src/taxonomy/tree-table/CreateRow.tsx | 167 +------------------- src/taxonomy/tree-table/DraftRow.tsx | 126 +++++++++++++++ src/taxonomy/tree-table/EditRow.tsx | 49 ++++++ src/taxonomy/tree-table/NestedRows.tsx | 78 +++++---- src/taxonomy/tree-table/SaveErrorAlert.tsx | 42 +++++ src/taxonomy/tree-table/TableBody.tsx | 54 +++---- src/taxonomy/tree-table/TableView.tsx | 23 +-- 14 files changed, 368 insertions(+), 303 deletions(-) create mode 100644 src/taxonomy/tag-list/UsageCountDisplay.tsx create mode 100644 src/taxonomy/tag-list/types.ts create mode 100644 src/taxonomy/tree-table/DraftRow.tsx create mode 100644 src/taxonomy/tree-table/EditRow.tsx create mode 100644 src/taxonomy/tree-table/SaveErrorAlert.tsx diff --git a/src/messages.ts b/src/messages.ts index 1620914b5b..06f7ffd519 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -10,6 +10,10 @@ const messages = defineMessages({ id: 'authoring.alert.support.text', defaultMessage: 'Support Page', }, + unknownError: { + id: 'authoring.alert.error.unknown', + defaultMessage: 'Unknown error', + }, }); export default messages; diff --git a/src/taxonomy/data/apiHooks.ts b/src/taxonomy/data/apiHooks.ts index 4fe1a3d06f..fa71a113b3 100644 --- a/src/taxonomy/data/apiHooks.ts +++ b/src/taxonomy/data/apiHooks.ts @@ -229,18 +229,13 @@ export const useSubTags = (taxonomyId: number, parentTagValue: string) => export const useCreateTag = (taxonomyId: number) => { const queryClient = useQueryClient(); - const intl = useIntl(); return useMutation({ - mutationFn: async ({ value, parentTagValue }: { value: string; parentTagValue?: string; }) => { - try { - await getAuthenticatedHttpClient().post( - apiUrls.createTag(taxonomyId), - { tag: value, parent_tag_value: parentTagValue }, - ); - } catch (err) { - throw new Error(getApiErrorMessage(err, intl)); - } + mutationFn: async ({ value, parentTagValue }: { value: string, parentTagValue?: string }) => { + await getAuthenticatedHttpClient().post( + apiUrls.createTag(taxonomyId), + { tag: value, parent_tag_value: parentTagValue }, + ); }, onSuccess: () => { queryClient.invalidateQueries({ @@ -260,15 +255,11 @@ export const useUpdateTag = (taxonomyId: number) => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ value, originalValue }: { value: string; originalValue: string; }) => { - try { - await getAuthenticatedHttpClient().patch( - apiUrls.updateTag(taxonomyId), - { tag: originalValue, updated_tag_value: value }, - ); - } catch (err) { - throw new Error(getApiErrorMessage(err)); - } + mutationFn: async ({ value, originalValue }: { value: string, originalValue: string }) => { + await getAuthenticatedHttpClient().patch( + apiUrls.updateTag(taxonomyId), + { tag: originalValue, updated_tag_value: value }, + ); }, onSuccess: () => { queryClient.invalidateQueries({ diff --git a/src/taxonomy/tag-list/UsageCountDisplay.tsx b/src/taxonomy/tag-list/UsageCountDisplay.tsx new file mode 100644 index 0000000000..97a95ad026 --- /dev/null +++ b/src/taxonomy/tag-list/UsageCountDisplay.tsx @@ -0,0 +1,25 @@ +import { + Bubble, +} from '@openedx/paragon'; +import type { Row } from '@tanstack/react-table'; +import type { + TreeRowData, +} from '@src/taxonomy/tree-table/types'; +import { TagListRowData } from './types'; + +const asTagListRowData = (row: Row): TagListRowData => ( + row.original as unknown as TagListRowData +); + +const UsageCountDisplay = ({ row }: { row: Row }) => { + const count = asTagListRowData(row).usageCount ?? 0; + return ( + count > 0 && ( + + {count} + + ) + ); +}; + +export default UsageCountDisplay; diff --git a/src/taxonomy/tag-list/hooks.ts b/src/taxonomy/tag-list/hooks.ts index c6a4f12c42..b8534af183 100644 --- a/src/taxonomy/tag-list/hooks.ts +++ b/src/taxonomy/tag-list/hooks.ts @@ -167,6 +167,26 @@ const useEditActions = ({ return true; }; + const getErrorMessage = (error: any): string => { + let errorMessage: string = ''; + if (error.name === 'AxiosError') { + const responseData = error.response?.data; + const tagError = Object.entries(responseData)?.find((errItem: [string, unknown]) => ( + ['tag', 'value', 'updated_tag_value'].includes(errItem[0].toLowerCase()) + )); + + const errorMessages = tagError ? tagError[1] : ( + (error as Error).message || intl.formatMessage(globalMessages.unknownError) + ); + errorMessage = Array.isArray(errorMessages) ? errorMessages.join('; ') : String(errorMessages); + } else { + errorMessage = (error as Error).message || intl.formatMessage(globalMessages.unknownError); + } + + errorMessage = errorMessage.replace(/\.$/, ''); // Remove trailing period for better message formatting + return errorMessage; + }; + const handleCreateTag = async (value: string, parentTagValue?: string) => { const trimmed = value.trim(); @@ -186,11 +206,9 @@ const useEditActions = ({ setIsCreatingTopTag(false); setCreatingParentId(null); } catch (error) { - const message = intl.formatMessage(messages.tagCreationErrorMessage, { errorMessage: (error as Error)?.message }); - setDraftError( - (error as Error)?.message || intl.formatMessage(messages.tagCreationErrorMessage, { errorMessage: '' }), - ); - setToast({ show: true, message }); + const errorMessage = getErrorMessage(error); + setDraftError(errorMessage); + setToast({ show: true, message: intl.formatMessage(messages.tagCreationErrorMessage, { errorMessage }) }); } }; @@ -217,11 +235,9 @@ const useEditActions = ({ message: intl.formatMessage(messages.tagUpdateSuccessMessage, { name: trimmed }), }); } catch (error) { - const message = intl.formatMessage(messages.tagUpdateErrorMessage, { errorMessage: (error as Error)?.message }); - setDraftError( - (error as Error)?.message || intl.formatMessage(messages.tagUpdateErrorMessage, { errorMessage: '' }), - ); - setToast({ show: true, message }); + const errorMessage = getErrorMessage(error); + setDraftError(errorMessage); + setToast({ show: true, message: intl.formatMessage(messages.tagUpdateErrorMessage, { errorMessage }) }); } }; diff --git a/src/taxonomy/tag-list/tagColumns.tsx b/src/taxonomy/tag-list/tagColumns.tsx index a7b459861a..363a1a9a12 100644 --- a/src/taxonomy/tag-list/tagColumns.tsx +++ b/src/taxonomy/tag-list/tagColumns.tsx @@ -1,5 +1,4 @@ import { - Bubble, Icon, IconButton, IconButtonWithTooltip, @@ -12,24 +11,18 @@ import { import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import type { Row } from '@tanstack/react-table'; -import messages from './messages'; import type { RowId, TreeColumnDef, TreeRowData, -} from '../tree-table/types'; +} from '@src/taxonomy/tree-table/types'; +import { TagListRowData } from './types'; +import messages from './messages'; import OptionalExpandLink from './OptionalExpandLink'; +import UsageCountDisplay from './UsageCountDisplay'; const EDITABLE_COLUMNS = ['value']; -interface TagListRowData extends TreeRowData { - depth: number; - childCount: number; - usageCount?: number; - isNew?: boolean; - isEditing?: boolean; -} - const asTagListRowData = (row: Row): TagListRowData => ( row.original as unknown as TagListRowData ); @@ -49,17 +42,6 @@ interface GetColumnsArgs { maxDepth: number; } -const UsageCountDisplay = ({ row }: { row: Row; }) => { - const count = asTagListRowData(row).usageCount ?? 0; - return ( - count > 0 && ( - - {count} - - ) - ); -}; - interface ActionsHeaderProps { onStartDraft: () => void; setDraftError: (error: string) => void; diff --git a/src/taxonomy/tag-list/types.ts b/src/taxonomy/tag-list/types.ts new file mode 100644 index 0000000000..845cee6956 --- /dev/null +++ b/src/taxonomy/tag-list/types.ts @@ -0,0 +1,9 @@ +import { TreeRowData } from '@src/taxonomy/tree-table/types'; + +export interface TagListRowData extends TreeRowData { + depth: number; + childCount: number; + usageCount?: number; + isNew?: boolean; + isEditing?: boolean; +} diff --git a/src/taxonomy/tree-table/CreateRow.test.tsx b/src/taxonomy/tree-table/CreateRow.test.tsx index cca6f8a18f..57a7840441 100644 --- a/src/taxonomy/tree-table/CreateRow.test.tsx +++ b/src/taxonomy/tree-table/CreateRow.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import { fireEvent, render, screen } from '@testing-library/react'; -import { CreateRow } from './CreateRow'; +import CreateRow from './CreateRow'; const wrapper = ({ children }: { children: React.ReactNode; }) => ( {children} @@ -15,7 +15,6 @@ const baseProps = () => ({ setIsCreatingTopRow: jest.fn(), exitDraftWithoutSave: jest.fn(), createRowMutation: { isPending: false }, - columns: [{ id: 'value' }], validate: jest.fn((value: string) => value.trim().length > 0), }); diff --git a/src/taxonomy/tree-table/CreateRow.tsx b/src/taxonomy/tree-table/CreateRow.tsx index 4091db0a56..f130560b02 100644 --- a/src/taxonomy/tree-table/CreateRow.tsx +++ b/src/taxonomy/tree-table/CreateRow.tsx @@ -1,24 +1,6 @@ -import React, { useState } from 'react'; -import { Button, Spinner } from '@openedx/paragon'; -import { useIntl } from '@edx/frontend-platform/i18n'; - -import { EditableCell } from './EditableCell'; -import type { CreateRowMutationState, TreeColumnDef } from './types'; -import messages from './messages'; - -interface DraftRowProps { - draftError: string; - initialValue?: string; - onSave: (value: string) => void; - onCancel: () => void; - mutationState: CreateRowMutationState; - columns: TreeColumnDef[]; - indent?: number; - validate: (value: string, mode?: 'soft' | 'hard') => boolean; - requireValueChangeToEnableSave?: boolean; - rowTestId?: string; - rowId?: string; -} +import React from 'react'; +import type { CreateRowMutationState } from './types'; +import DraftRow from './DraftRow'; interface CreateRowProps { draftError: string; @@ -27,118 +9,10 @@ interface CreateRowProps { setIsCreatingTopRow: (isCreating: boolean) => void; exitDraftWithoutSave: () => void; createRowMutation: CreateRowMutationState; - columns: TreeColumnDef[]; indent?: number; validate: (value: string, mode?: 'soft' | 'hard') => boolean; } -interface EditRowProps { - draftError: string; - setDraftError: (error: string) => void; - initialValue: string; - handleUpdateRow: (value: string) => void; - cancelEditRow: () => void; - updateRowMutation: CreateRowMutationState; - columns: TreeColumnDef[]; - indent?: number; - validate: (value: string, mode?: 'soft' | 'hard') => boolean; -} - -const DraftRow: React.FC = ({ - draftError, - initialValue = '', - onSave, - onCancel, - mutationState, - columns, - indent = 0, - validate, - requireValueChangeToEnableSave = false, - rowTestId, - rowId, -}) => { - const [rowValue, setRowValue] = useState(initialValue); - const [saveDisabled, setSaveDisabled] = useState(true); - const intl = useIntl(); - - const updateSaveDisabled = (value: string) => { - const trimmedValue = value.trim(); - const isValid = validate(value, 'soft'); - const isUnchanged = requireValueChangeToEnableSave && trimmedValue === initialValue.trim(); - setSaveDisabled(!isValid || !trimmedValue || isUnchanged || mutationState.isPending || false); - }; - - const handleValueChange = (e: React.ChangeEvent) => { - const { value } = e.target; - setRowValue(value); - updateSaveDisabled(value); - }; - - const handleSave = () => { - onSave(rowValue.trim()); - }; - - const handleValueCellKeyPress = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !saveDisabled && !draftError) { - e.preventDefault(); - handleSave(); - return; - } - - if (e.key === 'Escape') { - e.preventDefault(); - onCancel(); - } - }; - - return ( - - -
- -
- - - - - - - - - - {mutationState.isPending && ( - - )} - - - - ); -}; - const CreateRow: React.FC = ({ draftError, setDraftError, @@ -146,7 +20,6 @@ const CreateRow: React.FC = ({ setIsCreatingTopRow, exitDraftWithoutSave, createRowMutation, - columns, indent = 0, validate, }) => { @@ -162,7 +35,6 @@ const CreateRow: React.FC = ({ onSave={handleCreateRow} onCancel={handleCancel} mutationState={createRowMutation} - columns={columns} indent={indent} validate={validate} rowId="creating-top-row" @@ -171,35 +43,4 @@ const CreateRow: React.FC = ({ ); }; -const EditRow: React.FC = ({ - draftError, - setDraftError, - initialValue, - handleUpdateRow, - cancelEditRow, - updateRowMutation, - columns, - indent = 0, - validate, -}) => { - const handleCancel = () => { - setDraftError(''); - cancelEditRow(); - }; - - return ( - - ); -}; - -export { CreateRow, EditRow }; +export default CreateRow; diff --git a/src/taxonomy/tree-table/DraftRow.tsx b/src/taxonomy/tree-table/DraftRow.tsx new file mode 100644 index 0000000000..f15bc4f4a5 --- /dev/null +++ b/src/taxonomy/tree-table/DraftRow.tsx @@ -0,0 +1,126 @@ +import React, { useState } from 'react'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { Button, Spinner } from '@openedx/paragon'; +import { Row } from '@tanstack/react-table'; + +import UsageCountDisplay from '@src/taxonomy/tag-list/UsageCountDisplay'; +import { EditableCell } from './EditableCell'; +import type { CreateRowMutationState, TreeRowData } from './types'; +import messages from './messages'; + +interface DraftRowProps { + draftError: string; + initialValue?: string; + onSave: (value: string) => void; + onCancel: () => void; + mutationState: CreateRowMutationState; + indent?: number; + validate: (value: string, mode?: 'soft' | 'hard') => boolean; + requireValueChangeToEnableSave?: boolean; + rowTestId?: string; + rowId?: string; + row?: Row; +} + +const DraftRow: React.FC = ({ + draftError, + initialValue = '', + onSave, + onCancel, + mutationState, + indent = 0, + validate, + requireValueChangeToEnableSave = false, + rowTestId, + rowId, + row, + +}) => { + const [rowValue, setRowValue] = useState(initialValue); + const [saveDisabled, setSaveDisabled] = useState(true); + const intl = useIntl(); + + const updateSaveDisabled = (value: string) => { + const trimmedValue = value.trim(); + const isValid = validate(value, 'soft'); + const isUnchanged = requireValueChangeToEnableSave && trimmedValue === initialValue.trim(); + setSaveDisabled(!isValid || !trimmedValue || isUnchanged || mutationState.isPending || false); + }; + + const handleValueChange = (e: React.ChangeEvent) => { + const { value } = e.target; + setRowValue(value); + updateSaveDisabled(value); + }; + + const handleSave = () => { + onSave(rowValue.trim()); + }; + + const handleValueCellKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !saveDisabled && !draftError) { + e.preventDefault(); + handleSave(); + return; + } + + if (e.key === 'Escape') { + e.preventDefault(); + onCancel(); + } + }; + + const indentClass = indent > 0 ? `tree-table-indent tree-table-indent-${indent}` : ''; + + return ( + + +
+ +
+ + + {row ? : null} + + + + + + + + + + {mutationState.isPending && ( + + )} + + + + ); +}; + +export default DraftRow; diff --git a/src/taxonomy/tree-table/EditRow.tsx b/src/taxonomy/tree-table/EditRow.tsx new file mode 100644 index 0000000000..12a83fa055 --- /dev/null +++ b/src/taxonomy/tree-table/EditRow.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { Row } from '@tanstack/react-table'; +import type { CreateRowMutationState, TreeRowData } from './types'; +import DraftRow from './DraftRow'; + +interface EditRowProps { + draftError: string; + setDraftError: (error: string) => void; + initialValue: string; + handleUpdateRow: (value: string) => void; + cancelEditRow: () => void; + updateRowMutation: CreateRowMutationState; + indent?: number; + validate: (value: string, mode?: 'soft' | 'hard') => boolean; + row: Row; +} + +const EditRow: React.FC = ({ + draftError, + setDraftError, + initialValue, + handleUpdateRow, + cancelEditRow, + updateRowMutation, + indent = 0, + validate, + row, +}) => { + const handleCancel = () => { + setDraftError(''); + cancelEditRow(); + }; + + return ( + + ); +}; + +export default EditRow; diff --git a/src/taxonomy/tree-table/NestedRows.tsx b/src/taxonomy/tree-table/NestedRows.tsx index b3735e00c3..97565bf06b 100644 --- a/src/taxonomy/tree-table/NestedRows.tsx +++ b/src/taxonomy/tree-table/NestedRows.tsx @@ -7,7 +7,8 @@ import type { TreeColumnDef, CreateRowMutationState, } from './types'; -import { CreateRow, EditRow } from './CreateRow'; +import CreateRow from './CreateRow'; +import EditRow from './EditRow'; interface NestedRowsProps { /** The parent row object from TanStack React Table */ @@ -103,7 +104,6 @@ const NestedRows = ({ setIsCreatingTopRow={setIsCreatingTopRow} exitDraftWithoutSave={onCancelCreation} createRowMutation={createRowMutation} - columns={[]} indent={indent} validate={validate} /> @@ -112,45 +112,43 @@ const NestedRows = ({ const rowData = row.original || row; return ( - {editingRowId === `${row.original.id}:${String(row.original.value)}` ? - ( - handleUpdateRow(value, String(row.original.value))} - cancelEditRow={() => { - setEditingRowId(null); - exitDraftWithoutSave(); - }} - updateRowMutation={updateRowMutation} - columns={columns} - indent={indent} - validate={validate} - /> - ) : - ( - - {row.getVisibleCells() - .map((cell, index) => { - const content = flexRender(cell.column.columnDef.cell, cell.getContext()); - const isFirstColumn = index === 0; + {editingRowId === `${row.original.id}:${String(row.original.value)}` ? ( + handleUpdateRow(value, String(row.original.value))} + cancelEditRow={() => { + setEditingRowId(null); + exitDraftWithoutSave(); + }} + updateRowMutation={updateRowMutation} + indent={indent} + validate={validate} + row={row} + /> + ) : ( + + {row.getVisibleCells() + .map((cell, index) => { + const content = flexRender(cell.column.columnDef.cell, cell.getContext()); + const isFirstColumn = index === 0; - return ( - - {isFirstColumn ? -
{content}
: - content} - - ); - })} - - )} + return ( + + {isFirstColumn ? ( +
{content}
+ ) : ( + content + )} + + ); + })} + + )} { + const intl = useIntl(); + const hasError = (isError || isUpdateError) && !!draftError; + const [alertOpen, setAlertOpen] = React.useState(hasError); + + useEffect(() => { + if (hasError) { + setAlertOpen(true); + } + if (!hasError) { + setAlertOpen(false); + } + }, [hasError, isError, isUpdateError, draftError]); + + if (!alertOpen) { return null; } + + return ( + { setAlertOpen(false); }}> + + {intl.formatMessage(messages.errorSavingTitle)} + + {intl.formatMessage(messages.errorSavingMessage, { errorMessage: draftError }) } + + ); +}; + +export default SaveErrorAlert; diff --git a/src/taxonomy/tree-table/TableBody.tsx b/src/taxonomy/tree-table/TableBody.tsx index e7b42c3114..99fca02dd7 100644 --- a/src/taxonomy/tree-table/TableBody.tsx +++ b/src/taxonomy/tree-table/TableBody.tsx @@ -13,7 +13,8 @@ import type { TreeColumnDef, TreeTable, } from './types'; -import { CreateRow, EditRow } from './CreateRow'; +import CreateRow from './CreateRow'; +import EditRow from './EditRow'; interface TableBodyProps { columns: TreeColumnDef[]; @@ -86,39 +87,36 @@ const TableBody = ({ setIsCreatingTopRow={setIsCreatingTopRow} exitDraftWithoutSave={exitDraftWithoutSave} createRowMutation={createRowMutation} - columns={columns} validate={validate} /> )} {table.getRowModel().rows.filter(row => row.depth === 0).map(row => ( - {editingRowId === `${row.original.id}:${String(row.original.value)}` ? - ( - handleUpdateRow(value, String(row.original.value))} - cancelEditRow={() => { - setEditingRowId(null); - exitDraftWithoutSave(); - }} - updateRowMutation={updateRowMutation} - columns={columns} - validate={validate} - /> - ) : - ( - - {row.getVisibleCells() - .map((cell, index) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )} + {editingRowId === `${row.original.id}:${String(row.original.value)}` ? ( + handleUpdateRow(value, String(row.original.value))} + cancelEditRow={() => { + setEditingRowId(null); + exitDraftWithoutSave(); + }} + updateRowMutation={updateRowMutation} + validate={validate} + row={row} + /> + ) : ( + + {row.getVisibleCells() + .map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )} - {(isError || isUpdateError) && showError && ( - setShowError(false)} - > - - {intl.formatMessage(messages.errorSavingTitle)} - - {intl.formatMessage(messages.errorSavingMessage, { - errorMessage: draftError || intl.formatMessage(messages.errorSavingMessage, { errorMessage: '' }), - })} - - )} +
@@ -147,7 +132,7 @@ const TableView = ({ {headerGroup.headers.map((header, index) => ( {header.isPlaceholder ? null From 4217c2600e58ef5da1490ec15fa0fb15b0eb4d92 Mon Sep 17 00:00:00 2001 From: Jesper Hodge Date: Tue, 14 Apr 2026 12:24:17 -0400 Subject: [PATCH 02/12] fix: lint and cleanup --- src/taxonomy/data/apiHooks.ts | 1 - src/taxonomy/tag-list/OptionalExpandLink.tsx | 2 +- src/taxonomy/tag-list/TagListTable.tsx | 4 ++-- src/taxonomy/tag-list/hooks.ts | 5 +++-- src/taxonomy/tag-list/tagTree.ts | 2 +- src/taxonomy/tree-table/EditableCell.tsx | 3 ++- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/taxonomy/data/apiHooks.ts b/src/taxonomy/data/apiHooks.ts index fa71a113b3..67fd87932f 100644 --- a/src/taxonomy/data/apiHooks.ts +++ b/src/taxonomy/data/apiHooks.ts @@ -13,7 +13,6 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { camelCaseObject } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { useIntl } from '@edx/frontend-platform/i18n'; import { apiUrls, ALL_TAXONOMIES, getApiErrorMessage } from './api'; import * as api from './api'; import type { QueryOptions, TagListData } from './types'; diff --git a/src/taxonomy/tag-list/OptionalExpandLink.tsx b/src/taxonomy/tag-list/OptionalExpandLink.tsx index edfa6580f2..e42c198dcb 100644 --- a/src/taxonomy/tag-list/OptionalExpandLink.tsx +++ b/src/taxonomy/tag-list/OptionalExpandLink.tsx @@ -4,7 +4,7 @@ import { ExpandLess, ExpandMore } from '@openedx/paragon/icons'; import { Row } from '@tanstack/react-table'; import { useIntl } from '@edx/frontend-platform/i18n'; -import type { TreeRowData } from '../tree-table/types'; +import type { TreeRowData } from '@src/taxonomy/tree-table/types'; import messages from './messages'; interface OptionalExpandLinkProps { diff --git a/src/taxonomy/tag-list/TagListTable.tsx b/src/taxonomy/tag-list/TagListTable.tsx index c1a1a92dcf..c2d00319d8 100644 --- a/src/taxonomy/tag-list/TagListTable.tsx +++ b/src/taxonomy/tag-list/TagListTable.tsx @@ -4,9 +4,9 @@ import React, { useEffect, } from 'react'; import type { PaginationState } from '@tanstack/react-table'; -import { useTagListData, useCreateTag, useUpdateTag } from '../data/apiHooks'; +import { TableView } from '@src/taxonomy/tree-table'; +import { useTagListData, useCreateTag, useUpdateTag } from '@src/taxonomy/data/apiHooks'; import { TagTree } from './tagTree'; -import { TableView } from '../tree-table'; import type { RowId, TreeColumnDef, diff --git a/src/taxonomy/tag-list/hooks.ts b/src/taxonomy/tag-list/hooks.ts index b8534af183..56084ba54b 100644 --- a/src/taxonomy/tag-list/hooks.ts +++ b/src/taxonomy/tag-list/hooks.ts @@ -1,10 +1,11 @@ import { useReducer } from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { useCreateTag, useUpdateTag } from '../data/apiHooks'; +import globalMessages from '@src/messages'; +import { useCreateTag, useUpdateTag } from '@src/taxonomy/data/apiHooks'; +import type { RowId } from '@src/taxonomy/tree-table/types'; import { TagTree } from './tagTree'; import { TagListTableError } from './errors'; -import type { RowId } from '../tree-table/types'; import { TABLE_MODES, TRANSITION_TABLE, diff --git a/src/taxonomy/tag-list/tagTree.ts b/src/taxonomy/tag-list/tagTree.ts index fe68df0e66..eddf27d002 100644 --- a/src/taxonomy/tag-list/tagTree.ts +++ b/src/taxonomy/tag-list/tagTree.ts @@ -1,5 +1,5 @@ +import type { TagData } from '@src/taxonomy/data/types'; import { TagTreeError } from './errors'; -import type { TagData } from '../data/types'; export interface TagTreeNode extends TagData { subRows?: TagTreeNode[]; diff --git a/src/taxonomy/tree-table/EditableCell.tsx b/src/taxonomy/tree-table/EditableCell.tsx index 8cdc10aabc..d4de95fa74 100644 --- a/src/taxonomy/tree-table/EditableCell.tsx +++ b/src/taxonomy/tree-table/EditableCell.tsx @@ -7,8 +7,9 @@ import React, { import { Form } from '@openedx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; + +import OptionalExpandLink from '@src/taxonomy/tag-list/OptionalExpandLink'; import messages from './messages'; -import OptionalExpandLink from '../tag-list/OptionalExpandLink'; /** * Props for the EditableCell component. From 400240ccd9c9c3e72b2a52a9ae943b29fe86d01e Mon Sep 17 00:00:00 2001 From: Jesper Hodge Date: Tue, 14 Apr 2026 12:30:23 -0400 Subject: [PATCH 03/12] fix: UsageCountDisplay --- src/taxonomy/tag-list/UsageCountDisplay.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/taxonomy/tag-list/UsageCountDisplay.tsx b/src/taxonomy/tag-list/UsageCountDisplay.tsx index 97a95ad026..2471a43f5f 100644 --- a/src/taxonomy/tag-list/UsageCountDisplay.tsx +++ b/src/taxonomy/tag-list/UsageCountDisplay.tsx @@ -13,12 +13,15 @@ const asTagListRowData = (row: Row): TagListRowData => ( const UsageCountDisplay = ({ row }: { row: Row }) => { const count = asTagListRowData(row).usageCount ?? 0; + + if (count <= 0) { + return null; + } + return ( - count > 0 && ( - - {count} - - ) + + {count} + ); }; From 3c5d4e6949102cd6de5884ae1db60d7feb950740 Mon Sep 17 00:00:00 2001 From: Jesper Hodge Date: Tue, 14 Apr 2026 12:53:09 -0400 Subject: [PATCH 04/12] fix: tests --- src/taxonomy/data/apiHooks.test.jsx | 2 +- src/taxonomy/tag-list/TagListTable.test.jsx | 26 +++++++++++++++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/taxonomy/data/apiHooks.test.jsx b/src/taxonomy/data/apiHooks.test.jsx index 94120d1eff..f582a719c7 100644 --- a/src/taxonomy/data/apiHooks.test.jsx +++ b/src/taxonomy/data/apiHooks.test.jsx @@ -111,7 +111,7 @@ describe('import taxonomy api calls', () => { }); it('should surface duplicate tag error returned as an array', async () => { - const duplicateError = 'Tag with value \'ab\' already exists for taxonomy.'; + const duplicateError = 'Request failed with status code 400'; axiosMock.onPost(apiUrls.createTag(1)).reply(400, [duplicateError]); const { result } = renderHook(() => useCreateTag(1), { wrapper }); diff --git a/src/taxonomy/tag-list/TagListTable.test.jsx b/src/taxonomy/tag-list/TagListTable.test.jsx index 5b30ad9c25..cf263fd521 100644 --- a/src/taxonomy/tag-list/TagListTable.test.jsx +++ b/src/taxonomy/tag-list/TagListTable.test.jsx @@ -598,8 +598,17 @@ describe('', () => { expect(screen.getByText(/invalid character/i)).toBeInTheDocument(); }); - it('should show an inline duplicate-name error when the entered root tag already exists', async () => { - axiosMock.onPost(createTagUrl).reply(400, ['Tag with this name already exists']); + it('should show failure feedback when creating a duplicate root tag name', async () => { + axiosMock.onPost(createTagUrl).reply(() => { + const error = new Error('Request failed with status code 400'); + error.name = 'AxiosError'; + error.response = { + data: { + tag: ['Tag with this name already exists'], + }, + }; + return Promise.reject(error); + }); fireEvent.click(await screen.findByLabelText('Create Tag')); const draftRow = await screen.findAllByRole('row'); @@ -609,12 +618,19 @@ describe('', () => { fireEvent.change(input, { target: { value: 'root tag 1' } }); fireEvent.click(saveButton); - expect(await screen.findByText('Tag with this name already exists')).toBeInTheDocument(); + expect(await screen.findByText('Error creating tag: Tag with this name already exists')).toBeInTheDocument(); }); it('should keep the inline row and show a failure toast when save request fails', async () => { - axiosMock.onPost(createTagUrl).reply(500, { - error: 'Internal server error', + axiosMock.onPost(createTagUrl).reply(() => { + const error = new Error('Request failed with status code 500'); + error.name = 'AxiosError'; + error.response = { + data: { + tag: ['Internal server error'], + }, + }; + return Promise.reject(error); }); fireEvent.click(await screen.findByLabelText('Create Tag')); From a44a221a1d38f2263fae7fad58595332f63a5f43 Mon Sep 17 00:00:00 2001 From: Jesper Hodge Date: Tue, 14 Apr 2026 12:56:11 -0400 Subject: [PATCH 05/12] fix: test --- src/taxonomy/tree-table/TableView.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/taxonomy/tree-table/TableView.test.tsx b/src/taxonomy/tree-table/TableView.test.tsx index ff7b50cd88..eca55131b7 100644 --- a/src/taxonomy/tree-table/TableView.test.tsx +++ b/src/taxonomy/tree-table/TableView.test.tsx @@ -48,6 +48,7 @@ describe('TableView', () => { it('shows and dismisses save error banner', () => { const props = baseProps(); props.createRowMutation = { isPending: false, isError: true }; + props.draftError = 'Request failed with status code 500'; render(, { wrapper }); From 600a01c1b82fd8f3f1c299c0b55aac9bd3671500 Mon Sep 17 00:00:00 2001 From: Jesper Hodge Date: Wed, 15 Apr 2026 08:20:10 -0400 Subject: [PATCH 06/12] fix: lint --- src/taxonomy/data/apiHooks.ts | 4 +- src/taxonomy/tag-list/UsageCountDisplay.tsx | 2 +- src/taxonomy/tree-table/DraftRow.tsx | 1 - src/taxonomy/tree-table/NestedRows.tsx | 72 ++++++++++----------- src/taxonomy/tree-table/SaveErrorAlert.tsx | 11 +++- src/taxonomy/tree-table/TableBody.tsx | 50 +++++++------- 6 files changed, 74 insertions(+), 66 deletions(-) diff --git a/src/taxonomy/data/apiHooks.ts b/src/taxonomy/data/apiHooks.ts index 67fd87932f..b47ee23f1b 100644 --- a/src/taxonomy/data/apiHooks.ts +++ b/src/taxonomy/data/apiHooks.ts @@ -230,7 +230,7 @@ export const useCreateTag = (taxonomyId: number) => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ value, parentTagValue }: { value: string, parentTagValue?: string }) => { + mutationFn: async ({ value, parentTagValue }: { value: string; parentTagValue?: string; }) => { await getAuthenticatedHttpClient().post( apiUrls.createTag(taxonomyId), { tag: value, parent_tag_value: parentTagValue }, @@ -254,7 +254,7 @@ export const useUpdateTag = (taxonomyId: number) => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ value, originalValue }: { value: string, originalValue: string }) => { + mutationFn: async ({ value, originalValue }: { value: string; originalValue: string; }) => { await getAuthenticatedHttpClient().patch( apiUrls.updateTag(taxonomyId), { tag: originalValue, updated_tag_value: value }, diff --git a/src/taxonomy/tag-list/UsageCountDisplay.tsx b/src/taxonomy/tag-list/UsageCountDisplay.tsx index 2471a43f5f..b7bff8a482 100644 --- a/src/taxonomy/tag-list/UsageCountDisplay.tsx +++ b/src/taxonomy/tag-list/UsageCountDisplay.tsx @@ -11,7 +11,7 @@ const asTagListRowData = (row: Row): TagListRowData => ( row.original as unknown as TagListRowData ); -const UsageCountDisplay = ({ row }: { row: Row }) => { +const UsageCountDisplay = ({ row }: { row: Row; }) => { const count = asTagListRowData(row).usageCount ?? 0; if (count <= 0) { diff --git a/src/taxonomy/tree-table/DraftRow.tsx b/src/taxonomy/tree-table/DraftRow.tsx index f15bc4f4a5..22df64b8b9 100644 --- a/src/taxonomy/tree-table/DraftRow.tsx +++ b/src/taxonomy/tree-table/DraftRow.tsx @@ -34,7 +34,6 @@ const DraftRow: React.FC = ({ rowTestId, rowId, row, - }) => { const [rowValue, setRowValue] = useState(initialValue); const [saveDisabled, setSaveDisabled] = useState(true); diff --git a/src/taxonomy/tree-table/NestedRows.tsx b/src/taxonomy/tree-table/NestedRows.tsx index 97565bf06b..c2647f9f98 100644 --- a/src/taxonomy/tree-table/NestedRows.tsx +++ b/src/taxonomy/tree-table/NestedRows.tsx @@ -112,43 +112,43 @@ const NestedRows = ({ const rowData = row.original || row; return ( - {editingRowId === `${row.original.id}:${String(row.original.value)}` ? ( - handleUpdateRow(value, String(row.original.value))} - cancelEditRow={() => { - setEditingRowId(null); - exitDraftWithoutSave(); - }} - updateRowMutation={updateRowMutation} - indent={indent} - validate={validate} - row={row} - /> - ) : ( - - {row.getVisibleCells() - .map((cell, index) => { - const content = flexRender(cell.column.columnDef.cell, cell.getContext()); - const isFirstColumn = index === 0; + {editingRowId === `${row.original.id}:${String(row.original.value)}` ? + ( + handleUpdateRow(value, String(row.original.value))} + cancelEditRow={() => { + setEditingRowId(null); + exitDraftWithoutSave(); + }} + updateRowMutation={updateRowMutation} + indent={indent} + validate={validate} + row={row} + /> + ) : + ( + + {row.getVisibleCells() + .map((cell, index) => { + const content = flexRender(cell.column.columnDef.cell, cell.getContext()); + const isFirstColumn = index === 0; - return ( - - {isFirstColumn ? ( -
{content}
- ) : ( - content - )} - - ); - })} - - )} + return ( + + {isFirstColumn ? +
{content}
: + content} + + ); + })} + + )} { setAlertOpen(false); }}> + { + setAlertOpen(false); + }} + > {intl.formatMessage(messages.errorSavingTitle)} - {intl.formatMessage(messages.errorSavingMessage, { errorMessage: draftError }) } + {intl.formatMessage(messages.errorSavingMessage, { errorMessage: draftError })} ); }; diff --git a/src/taxonomy/tree-table/TableBody.tsx b/src/taxonomy/tree-table/TableBody.tsx index 99fca02dd7..0962c4c6c7 100644 --- a/src/taxonomy/tree-table/TableBody.tsx +++ b/src/taxonomy/tree-table/TableBody.tsx @@ -93,30 +93,32 @@ const TableBody = ({ {table.getRowModel().rows.filter(row => row.depth === 0).map(row => ( - {editingRowId === `${row.original.id}:${String(row.original.value)}` ? ( - handleUpdateRow(value, String(row.original.value))} - cancelEditRow={() => { - setEditingRowId(null); - exitDraftWithoutSave(); - }} - updateRowMutation={updateRowMutation} - validate={validate} - row={row} - /> - ) : ( - - {row.getVisibleCells() - .map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )} + {editingRowId === `${row.original.id}:${String(row.original.value)}` ? + ( + handleUpdateRow(value, String(row.original.value))} + cancelEditRow={() => { + setEditingRowId(null); + exitDraftWithoutSave(); + }} + updateRowMutation={updateRowMutation} + validate={validate} + row={row} + /> + ) : + ( + + {row.getVisibleCells() + .map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )} Date: Wed, 15 Apr 2026 15:28:03 -0400 Subject: [PATCH 07/12] fix: test accuracy --- src/taxonomy/data/apiHooks.test.jsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/taxonomy/data/apiHooks.test.jsx b/src/taxonomy/data/apiHooks.test.jsx index f582a719c7..f80d2c9025 100644 --- a/src/taxonomy/data/apiHooks.test.jsx +++ b/src/taxonomy/data/apiHooks.test.jsx @@ -110,11 +110,19 @@ describe('import taxonomy api calls', () => { expect(axiosMock.history.put[0].url).toEqual(apiUrls.tagsPlanImport(1)); }); - it('should surface duplicate tag error returned as an array', async () => { - const duplicateError = 'Request failed with status code 400'; - axiosMock.onPost(apiUrls.createTag(1)).reply(400, [duplicateError]); + it('should surface tag errors', async () => { + const duplicateMessage = 'Tag with value \'ab\' already exists for taxonomy.'; + axiosMock.onPost(apiUrls.createTag(1)).reply(400, [duplicateMessage]); const { result } = renderHook(() => useCreateTag(1), { wrapper }); - await expect(result.current.mutateAsync({ value: 'ab' })).rejects.toEqual(Error(duplicateError)); + try { + await result.current.mutateAsync({ value: 'ab' }); + // expect: if code reaches this line, the test should fail because an error should have been thrown + expect('This line should not be reached').toBe(false); + } catch (error) { + // we check the response data, not the error message, because of how react-query surfaces errors from axios + // @ts-ignore + expect(error.response.data).toEqual([duplicateMessage]); + } }); }); From 0bb89a137818e13ad9d31a6d4709420d4d897164 Mon Sep 17 00:00:00 2001 From: Jesper Hodge Date: Fri, 17 Apr 2026 11:43:55 -0400 Subject: [PATCH 08/12] fix: typecheck for AxiosError --- src/taxonomy/tag-list/hooks.ts | 43 +++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/src/taxonomy/tag-list/hooks.ts b/src/taxonomy/tag-list/hooks.ts index 56084ba54b..02735b25ae 100644 --- a/src/taxonomy/tag-list/hooks.ts +++ b/src/taxonomy/tag-list/hooks.ts @@ -1,4 +1,5 @@ import { useReducer } from 'react'; +import { AxiosError } from 'axios'; import { useIntl } from '@edx/frontend-platform/i18n'; import globalMessages from '@src/messages'; @@ -168,24 +169,34 @@ const useEditActions = ({ return true; }; - const getErrorMessage = (error: any): string => { - let errorMessage: string = ''; - if (error.name === 'AxiosError') { - const responseData = error.response?.data; - const tagError = Object.entries(responseData)?.find((errItem: [string, unknown]) => ( - ['tag', 'value', 'updated_tag_value'].includes(errItem[0].toLowerCase()) - )); - - const errorMessages = tagError ? tagError[1] : ( - (error as Error).message || intl.formatMessage(globalMessages.unknownError) - ); - errorMessage = Array.isArray(errorMessages) ? errorMessages.join('; ') : String(errorMessages); - } else { - errorMessage = (error as Error).message || intl.formatMessage(globalMessages.unknownError); + const formatErrorMessage = (errorMessage: string): string => { + // Remove trailing period for better message formatting + return errorMessage.replace(/\.$/, ''); + }; + + const getAxiosErrorMessage = (axiosError: AxiosError) => { + const responseData = axiosError.response?.data; + const tagError = responseData ? Object.entries(responseData)?.find((errItem: [string, unknown]) => ( + ['tag', 'value', 'updated_tag_value'].includes(errItem[0].toLowerCase()) + )) : null; + + const errorMessages = tagError ? tagError[1] : ( + axiosError.message || intl.formatMessage(globalMessages.unknownError) + ); + const errorMessage = Array.isArray(errorMessages) ? errorMessages.join('; ') : String(errorMessages); + return formatErrorMessage(errorMessage); + }; + + const getErrorMessage = (error: unknown): string => { + if (error instanceof AxiosError) { + return getAxiosErrorMessage(error); + } + + if (error instanceof Error && error.message) { + return formatErrorMessage(error.message); } - errorMessage = errorMessage.replace(/\.$/, ''); // Remove trailing period for better message formatting - return errorMessage; + return intl.formatMessage(globalMessages.unknownError); }; const handleCreateTag = async (value: string, parentTagValue?: string) => { From 98b5cf9e4d3c784f34d83e95c10e3c3bb79bee0a Mon Sep 17 00:00:00 2001 From: Jesper Hodge Date: Fri, 17 Apr 2026 12:12:12 -0400 Subject: [PATCH 09/12] fix: PR comments --- src/taxonomy/tag-list/TagListTable.test.jsx | 7 +++---- src/taxonomy/tag-list/UsageCountDisplay.tsx | 7 ++----- src/taxonomy/tag-list/tagColumns.tsx | 11 ++++------- src/taxonomy/tag-list/utils.ts | 12 ++++++++++++ src/taxonomy/tree-table/DraftRow.tsx | 11 ++++------- src/taxonomy/tree-table/TableView.scss | 6 +----- 6 files changed, 26 insertions(+), 28 deletions(-) create mode 100644 src/taxonomy/tag-list/utils.ts diff --git a/src/taxonomy/tag-list/TagListTable.test.jsx b/src/taxonomy/tag-list/TagListTable.test.jsx index cf263fd521..d9022c84fb 100644 --- a/src/taxonomy/tag-list/TagListTable.test.jsx +++ b/src/taxonomy/tag-list/TagListTable.test.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { AxiosError } from 'axios'; import { render, waitFor, @@ -600,8 +601,7 @@ describe('', () => { it('should show failure feedback when creating a duplicate root tag name', async () => { axiosMock.onPost(createTagUrl).reply(() => { - const error = new Error('Request failed with status code 400'); - error.name = 'AxiosError'; + const error = new AxiosError('Request failed with status code 400'); error.response = { data: { tag: ['Tag with this name already exists'], @@ -623,8 +623,7 @@ describe('', () => { it('should keep the inline row and show a failure toast when save request fails', async () => { axiosMock.onPost(createTagUrl).reply(() => { - const error = new Error('Request failed with status code 500'); - error.name = 'AxiosError'; + const error = new AxiosError('Request failed with status code 500'); error.response = { data: { tag: ['Internal server error'], diff --git a/src/taxonomy/tag-list/UsageCountDisplay.tsx b/src/taxonomy/tag-list/UsageCountDisplay.tsx index b7bff8a482..ac6d45d9ba 100644 --- a/src/taxonomy/tag-list/UsageCountDisplay.tsx +++ b/src/taxonomy/tag-list/UsageCountDisplay.tsx @@ -6,13 +6,10 @@ import type { TreeRowData, } from '@src/taxonomy/tree-table/types'; import { TagListRowData } from './types'; - -const asTagListRowData = (row: Row): TagListRowData => ( - row.original as unknown as TagListRowData -); +import { getTagListRowData } from './utils'; const UsageCountDisplay = ({ row }: { row: Row; }) => { - const count = asTagListRowData(row).usageCount ?? 0; + const count = getTagListRowData(row).usageCount ?? 0; if (count <= 0) { return null; diff --git a/src/taxonomy/tag-list/tagColumns.tsx b/src/taxonomy/tag-list/tagColumns.tsx index 363a1a9a12..58f4a2dc99 100644 --- a/src/taxonomy/tag-list/tagColumns.tsx +++ b/src/taxonomy/tag-list/tagColumns.tsx @@ -20,13 +20,10 @@ import { TagListRowData } from './types'; import messages from './messages'; import OptionalExpandLink from './OptionalExpandLink'; import UsageCountDisplay from './UsageCountDisplay'; +import { getTagListRowData } from './utils'; const EDITABLE_COLUMNS = ['value']; -const asTagListRowData = (row: Row): TagListRowData => ( - row.original as unknown as TagListRowData -); - interface GetColumnsArgs { setIsCreatingTopTag: (isCreating: boolean) => void; setCreatingParentId: (id: RowId | null) => void; @@ -157,7 +154,7 @@ function getColumns({ cell: ({ row }) => { const { value, - } = asTagListRowData(row); + } = getTagListRowData(row); return ( @@ -187,14 +184,14 @@ function getColumns({ /> ), cell: ({ row }) => { - const rowData = asTagListRowData(row); + const rowData = getTagListRowData(row); if (rowData.isNew || rowData.isEditing) { return
; } const disableAddSubtag = hasOpenDraft || !canAddTag; - const disableEditTag = hasOpenDraft || row.original.canChangeTag === false; + const disableEditTag = hasOpenDraft || rowData.canChangeTag === false; const startSubtagDraft = () => { onStartDraft(); diff --git a/src/taxonomy/tag-list/utils.ts b/src/taxonomy/tag-list/utils.ts new file mode 100644 index 0000000000..f99f4becdc --- /dev/null +++ b/src/taxonomy/tag-list/utils.ts @@ -0,0 +1,12 @@ +import { Row } from '@openedx/paragon'; +import { TreeRowData } from '@src/taxonomy/tree-table/types'; +import { TagListRowData } from './types'; + +/** getTagListRowData + * + * Minimal getter function for `row.original`. Mainly because the naming of `original` is not expressive, + * and it needs to be cast to the correct type. +*/ +export const getTagListRowData = (row: Row): TagListRowData => ( + row.original as unknown as TagListRowData +); diff --git a/src/taxonomy/tree-table/DraftRow.tsx b/src/taxonomy/tree-table/DraftRow.tsx index 22df64b8b9..3157e4d25f 100644 --- a/src/taxonomy/tree-table/DraftRow.tsx +++ b/src/taxonomy/tree-table/DraftRow.tsx @@ -69,12 +69,10 @@ const DraftRow: React.FC = ({ } }; - const indentClass = indent > 0 ? `tree-table-indent tree-table-indent-${indent}` : ''; - return ( - -
+ +
= ({ />
- + {row ? : null} diff --git a/src/taxonomy/tree-table/TableView.scss b/src/taxonomy/tree-table/TableView.scss index 19647fadf2..706a0eb5f8 100644 --- a/src/taxonomy/tree-table/TableView.scss +++ b/src/taxonomy/tree-table/TableView.scss @@ -10,11 +10,7 @@ overflow-wrap: anywhere; } -.tree-table-indent { - padding-inline-start: var(--pgn-spacing-spacer-base); -} - -@for $depth from 2 through 10 { +@for $depth from 0 through 10 { .tree-table-indent-#{$depth} { padding-inline-start: calc(var(--pgn-spacing-spacer-base) * #{$depth}); } From 4d6fa4a8ccd1027160fd6684b27aa476ac8d96e0 Mon Sep 17 00:00:00 2001 From: Jesper Hodge <19345795+jesperhodge@users.noreply.github.com> Date: Fri, 17 Apr 2026 12:12:26 -0400 Subject: [PATCH 10/12] fix: Apply suggestions from code review --- src/taxonomy/tag-list/UsageCountDisplay.tsx | 2 +- src/taxonomy/tag-list/tagColumns.tsx | 2 +- src/taxonomy/tag-list/types.ts | 2 +- src/taxonomy/tree-table/SaveErrorAlert.tsx | 9 ++------- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/taxonomy/tag-list/UsageCountDisplay.tsx b/src/taxonomy/tag-list/UsageCountDisplay.tsx index ac6d45d9ba..343a6a9985 100644 --- a/src/taxonomy/tag-list/UsageCountDisplay.tsx +++ b/src/taxonomy/tag-list/UsageCountDisplay.tsx @@ -5,7 +5,7 @@ import type { Row } from '@tanstack/react-table'; import type { TreeRowData, } from '@src/taxonomy/tree-table/types'; -import { TagListRowData } from './types'; +import type { TagListRowData } from './types'; import { getTagListRowData } from './utils'; const UsageCountDisplay = ({ row }: { row: Row; }) => { diff --git a/src/taxonomy/tag-list/tagColumns.tsx b/src/taxonomy/tag-list/tagColumns.tsx index 58f4a2dc99..7b8874331f 100644 --- a/src/taxonomy/tag-list/tagColumns.tsx +++ b/src/taxonomy/tag-list/tagColumns.tsx @@ -16,7 +16,7 @@ import type { TreeColumnDef, TreeRowData, } from '@src/taxonomy/tree-table/types'; -import { TagListRowData } from './types'; +import type { TagListRowData } from './types'; import messages from './messages'; import OptionalExpandLink from './OptionalExpandLink'; import UsageCountDisplay from './UsageCountDisplay'; diff --git a/src/taxonomy/tag-list/types.ts b/src/taxonomy/tag-list/types.ts index 845cee6956..4c27d0e133 100644 --- a/src/taxonomy/tag-list/types.ts +++ b/src/taxonomy/tag-list/types.ts @@ -1,4 +1,4 @@ -import { TreeRowData } from '@src/taxonomy/tree-table/types'; +import type { TreeRowData } from '@src/taxonomy/tree-table/types'; export interface TagListRowData extends TreeRowData { depth: number; diff --git a/src/taxonomy/tree-table/SaveErrorAlert.tsx b/src/taxonomy/tree-table/SaveErrorAlert.tsx index 40d926b3da..d38465f2df 100644 --- a/src/taxonomy/tree-table/SaveErrorAlert.tsx +++ b/src/taxonomy/tree-table/SaveErrorAlert.tsx @@ -15,16 +15,11 @@ interface SaveErrorAlertProps { } const SaveErrorAlert = ({ draftError, isError, isUpdateError }: SaveErrorAlertProps) => { const intl = useIntl(); - const hasError = (isError || isUpdateError) && !!draftError; + const hasError: boolean = (isError || isUpdateError) && !!draftError; const [alertOpen, setAlertOpen] = React.useState(hasError); useEffect(() => { - if (hasError) { - setAlertOpen(true); - } - if (!hasError) { - setAlertOpen(false); - } + setAlertOpen(hasError); }, [hasError, isError, isUpdateError, draftError]); if (!alertOpen) { return null; } From 58ba86f08352abbfc8b0b5e89c6fc0806b7a0412 Mon Sep 17 00:00:00 2001 From: Jesper Hodge Date: Fri, 17 Apr 2026 12:21:34 -0400 Subject: [PATCH 11/12] fix: lint --- src/taxonomy/tag-list/UsageCountDisplay.tsx | 1 - src/taxonomy/tag-list/hooks.ts | 8 +++++--- src/taxonomy/tag-list/utils.ts | 2 +- src/taxonomy/tree-table/DraftRow.tsx | 4 +--- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/taxonomy/tag-list/UsageCountDisplay.tsx b/src/taxonomy/tag-list/UsageCountDisplay.tsx index 343a6a9985..d5ef03bb45 100644 --- a/src/taxonomy/tag-list/UsageCountDisplay.tsx +++ b/src/taxonomy/tag-list/UsageCountDisplay.tsx @@ -5,7 +5,6 @@ import type { Row } from '@tanstack/react-table'; import type { TreeRowData, } from '@src/taxonomy/tree-table/types'; -import type { TagListRowData } from './types'; import { getTagListRowData } from './utils'; const UsageCountDisplay = ({ row }: { row: Row; }) => { diff --git a/src/taxonomy/tag-list/hooks.ts b/src/taxonomy/tag-list/hooks.ts index 02735b25ae..a1518b9de9 100644 --- a/src/taxonomy/tag-list/hooks.ts +++ b/src/taxonomy/tag-list/hooks.ts @@ -176,9 +176,11 @@ const useEditActions = ({ const getAxiosErrorMessage = (axiosError: AxiosError) => { const responseData = axiosError.response?.data; - const tagError = responseData ? Object.entries(responseData)?.find((errItem: [string, unknown]) => ( - ['tag', 'value', 'updated_tag_value'].includes(errItem[0].toLowerCase()) - )) : null; + const tagError = responseData ? + Object.entries(responseData)?.find((errItem: [string, unknown]) => ( + ['tag', 'value', 'updated_tag_value'].includes(errItem[0].toLowerCase()) + )) : + null; const errorMessages = tagError ? tagError[1] : ( axiosError.message || intl.formatMessage(globalMessages.unknownError) diff --git a/src/taxonomy/tag-list/utils.ts b/src/taxonomy/tag-list/utils.ts index f99f4becdc..012ca2b824 100644 --- a/src/taxonomy/tag-list/utils.ts +++ b/src/taxonomy/tag-list/utils.ts @@ -6,7 +6,7 @@ import { TagListRowData } from './types'; * * Minimal getter function for `row.original`. Mainly because the naming of `original` is not expressive, * and it needs to be cast to the correct type. -*/ + */ export const getTagListRowData = (row: Row): TagListRowData => ( row.original as unknown as TagListRowData ); diff --git a/src/taxonomy/tree-table/DraftRow.tsx b/src/taxonomy/tree-table/DraftRow.tsx index 3157e4d25f..1be3b01a2c 100644 --- a/src/taxonomy/tree-table/DraftRow.tsx +++ b/src/taxonomy/tree-table/DraftRow.tsx @@ -86,9 +86,7 @@ const DraftRow: React.FC = ({ {row ? : null} - +