Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions src/taxonomy/data/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export const apiUrls = {
/** URL to plan (preview what would happen) a taxonomy import */
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There's plenty of horizontal space here. Could we make the text input wider, so that it's more often able to show the whole tag during editing?

Fine to ticket up for a followup PR if you'd rather do that.

View:
Screenshot 2026-03-31 at 10 14 15 AM

Edit:
Screenshot 2026-03-31 at 10 15 25 AM

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes! Good catch.
Added it to openedx/modular-learning#256

tagsPlanImport: (taxonomyId: number) => makeUrl(`${taxonomyId}/tags/import/plan/`),
createTag: (taxonomyId: number) => makeUrl(`${taxonomyId}/tags/`),
updateTag: (taxonomyId: number) => makeUrl(`${taxonomyId}/tags/`),
} satisfies Record<string, (...args: any[]) => string>;

/**
Expand Down
24 changes: 24 additions & 0 deletions src/taxonomy/data/apiHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,27 @@ export const useCreateTag = (taxonomyId: number) => {
},
});
};

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));
}
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: taxonomyQueryKeys.taxonomyTagList(taxonomyId),
});
// In the metadata, 'tagsCount' (and possibly other fields) will have changed:
queryClient.invalidateQueries({ queryKey: taxonomyQueryKeys.taxonomyMetadata(taxonomyId) });
},
});
};
1 change: 1 addition & 0 deletions src/taxonomy/data/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export interface TagListData {
next: string;
numPages: number;
previous: string;
canAddTag?: boolean;
results: TagData[];
start: number;
}
517 changes: 356 additions & 161 deletions src/taxonomy/tag-list/TagListTable.test.jsx

Large diffs are not rendered by default.

12 changes: 9 additions & 3 deletions src/taxonomy/tag-list/TagListTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React, {
useEffect,
} from 'react';
import type { PaginationState } from '@tanstack/react-table';
import { useTagListData, useCreateTag } from '../data/apiHooks';
import { useTagListData, useCreateTag, useUpdateTag } from '../data/apiHooks';
import { TagTree } from './tagTree';
import { TableView } from '../tree-table';
import type {
Expand Down Expand Up @@ -78,6 +78,7 @@ const TagListTable = ({ taxonomyId, maxDepth }: TagListTableProps) => {
enabled: tableMode === TABLE_MODES.VIEW,
});
const createTagMutation = useCreateTag(taxonomyId);
const updateTagMutation = useUpdateTag(taxonomyId);
const pageCount = tagList?.numPages ?? -1;

// TODO: to make this more readable, introduce a React context for the TagListTable instead of passing props.
Expand All @@ -88,6 +89,7 @@ const TagListTable = ({ taxonomyId, maxDepth }: TagListTableProps) => {
setTagTree,
setDraftError,
createTagMutation,
updateTagMutation,
enterPreviewMode,
setToast,
setIsCreatingTopTag,
Expand All @@ -105,19 +107,19 @@ const TagListTable = ({ taxonomyId, maxDepth }: TagListTableProps) => {
onStartDraft: enterDraftMode,
setActiveActionMenuRowId,
hasOpenDraft,
canAddTag: tagList?.canAddTag !== false,
draftError,
setDraftError,
isSavingDraft: createTagMutation.isPending,
maxDepth,
creatingParentId,
}),
[
isCreatingTopTag,
editingRowId,
tableMode,
activeActionMenuRowId,
hasOpenDraft,
creatingParentId,
tagList?.canAddTag,
draftError,
createTagMutation.isPending,
maxDepth,
Expand Down Expand Up @@ -155,7 +157,9 @@ const TagListTable = ({ taxonomyId, maxDepth }: TagListTableProps) => {
isCreatingTopRow: isCreatingTopTag,
draftError,
createRowMutation: createTagMutation,
updateRowMutation: updateTagMutation,
handleCreateRow: handleCreateTag,
handleUpdateRow: handleUpdateTag,
toast,
setToast,
setIsCreatingTopRow: setIsCreatingTopTag,
Expand All @@ -164,6 +168,8 @@ const TagListTable = ({ taxonomyId, maxDepth }: TagListTableProps) => {
setCreatingParentId,
setDraftError,
validate,
editingRowId,
setEditingRowId,
}}
/>
);
Expand Down
9 changes: 7 additions & 2 deletions src/taxonomy/tag-list/hooks.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { act, renderHook } from '@testing-library/react';
import { act, renderHook, waitFor } from '@testing-library/react';

import { TagTree } from './tagTree';
import { useEditActions, useTableModes } from './hooks';
Expand Down Expand Up @@ -44,6 +44,8 @@ describe('useTableModes', () => {
describe('useEditActions', () => {
const buildActions = (overrides = {}) => {
const createTagMutation = { mutateAsync: jest.fn() };
// mock updateTagMutation to have a function `mutateAsync` that returns a resolved promise
const updateTagMutation = { mutateAsync: jest.fn() };
const setTagTree = jest.fn();
const setDraftError = jest.fn();
const enterPreviewMode = jest.fn();
Expand All @@ -63,6 +65,7 @@ describe('useEditActions', () => {
setCreatingParentId,
exitDraftWithoutSave,
setEditingRowId,
updateTagMutation: updateTagMutation as any,
...(overrides as any),
};

Expand Down Expand Up @@ -139,7 +142,9 @@ describe('useEditActions', () => {
await actions.handleUpdateTag('updated', 'original');
});

expect(enterPreviewMode).toHaveBeenCalled();
await waitFor(() => {
expect(enterPreviewMode).toHaveBeenCalled();
});
expect(setToast).toHaveBeenCalledWith({
show: true,
message: 'Tag "updated" updated successfully',
Expand Down
35 changes: 32 additions & 3 deletions src/taxonomy/tag-list/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useReducer } from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';

import { useCreateTag } from '../data/apiHooks';
import { useCreateTag, useUpdateTag } from '../data/apiHooks';
import { TagTree } from './tagTree';
import { TagListTableError } from './errors';
import type { RowId } from '../tree-table/types';
Expand Down Expand Up @@ -45,6 +45,7 @@ interface UseEditActionsParams {
setCreatingParentId: React.Dispatch<React.SetStateAction<RowId | null>>;
exitDraftWithoutSave: () => void;
setEditingRowId: React.Dispatch<React.SetStateAction<RowId | null>>;
updateTagMutation: ReturnType<typeof useUpdateTag>;
}

const getInlineValidationMessage = (value: string, intl: ReturnType<typeof useIntl>): string => {
Expand Down Expand Up @@ -111,9 +112,20 @@ const useEditActions = ({
setToast,
setIsCreatingTopTag,
setCreatingParentId,
exitDraftWithoutSave,
setEditingRowId,
updateTagMutation,
}: UseEditActionsParams) => {
const intl = useIntl();

const updateTableAfterRename = (oldValue: string, newValue: string) => {
setTagTree((currentTagTree) => {
const nextTree = currentTagTree || new TagTree([]);
nextTree.editTagValue(oldValue, newValue);
return nextTree;
});
};

const updateTableWithoutDataReload = (value: string, parentTagValue: string | null = null) => {
setTagTree((currentTagTree) => {
const nextTree = currentTagTree || new TagTree([]);
Expand Down Expand Up @@ -179,14 +191,31 @@ const useEditActions = ({

const handleUpdateTag = async (value: string, originalValue: string) => {
const trimmed = value.trim();
if (trimmed && trimmed !== originalValue) {
if (!validate(trimmed, 'soft')) {
return;
}

if (trimmed === originalValue) {
setEditingRowId(null);
exitDraftWithoutSave();
return;
}

try {
setDraftError('');
await updateTagMutation.mutateAsync({ value: trimmed, originalValue });
updateTableAfterRename(originalValue, trimmed);
enterPreviewMode();
setEditingRowId(null);
setToast({
show: true,
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 });
}
setEditingRowId(null);
};

return {
Expand Down
8 changes: 8 additions & 0 deletions src/taxonomy/tag-list/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ const messages = defineMessages({
id: 'course-authoring.tag-list.hide-subtags.button-label',
defaultMessage: 'Hide Subtags',
},
tagUpdateErrorMessage: {
id: 'course-authoring.tag-list.update-error',
defaultMessage: 'Error updating tag: {errorMessage}',
},
renameTag: {
id: 'course-authoring.tag-list.rename-tag',
defaultMessage: 'Rename',
},
});

export default messages;
Loading