Skip to content
Draft
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
3 changes: 3 additions & 0 deletions src/content-tags-drawer/ContentTagsSnippet.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.tag-snippet-chip {
max-width: 260px;
}
67 changes: 67 additions & 0 deletions src/content-tags-drawer/ContentTagsSnippet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Chip, Stack } from '@openedx/paragon';
import { Tag as TagIcon } from '@openedx/paragon/icons';

import { useContentTaxonomyTagsData } from './data/apiHooks';
import { Tag } from './data/types';

interface ContentTagsSnippetProps {
contentId: string;
}

const ContentTagChip = ({ tag }: { tag: Tag }) => {
let lineageStr = tag.lineage.join(' > ');
const lineageLength = tag.lineage.length;
const MAX_TAG_LENGTH = 30;

if (lineageStr.length > MAX_TAG_LENGTH && lineageLength > 1) {
if (lineageLength > 2) {
// NOTE: If the tag lineage is too long and have more than 2 tags, we truncate it to the first and last level
// i.e "Abilities > Cognitive Abilities > Communication Abilities" becomes
// "Abilities > .. > Communication Abilities"
lineageStr = `${tag.lineage[0]} > .. > ${tag.lineage[lineageLength - 1]}`;
}

if (lineageStr.length > MAX_TAG_LENGTH) {
// NOTE: If the tag lineage is still too long, we truncate it only to the last level
// i.e "Knowledge > .. > Administration and Management" becomes
// ".. > Administration and Management"
lineageStr = `.. > ${tag.lineage[lineageLength - 1]}`;
}
}

return (
<Chip
iconBefore={TagIcon}
className="mr-1 tag-snippet-chip"
>
{lineageStr}
</Chip>
);
};

export const ContentTagsSnippet = ({ contentId }: ContentTagsSnippetProps) => {
const {
data,
} = useContentTaxonomyTagsData(contentId);

if (!data) {
return null;
}

return (
<Stack gap={2}>
{data.taxonomies.map((taxonomy) => (
<div key={taxonomy.taxonomyId}>
<h4 className="font-weight-bold x-small text-muted">
{`${taxonomy.name} (${taxonomy.tags.length})`}
</h4>
<div className="d-flex flex-wrap">
{taxonomy.tags.map((tag) => (
<ContentTagChip key={tag.value} tag={tag} />
))}
</div>
</div>
))}
</Stack>
);
};
20 changes: 0 additions & 20 deletions src/content-tags-drawer/TagOutlineIcon.tsx

This file was deleted.

101 changes: 0 additions & 101 deletions src/content-tags-drawer/data/api.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// @ts-check
import MockAdapter from 'axios-mock-adapter';
import { initializeMockApp } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
Expand Down Expand Up @@ -44,7 +43,7 @@ describe('content tags drawer api calls', () => {
});

it('should get taxonomy tags data', async () => {
const taxonomyId = 123;
const taxonomyId = '123';
axiosMock.onGet().reply(200, taxonomyTagsMock);
const result = await getTaxonomyTagsData(taxonomyId);

Expand All @@ -53,7 +52,7 @@ describe('content tags drawer api calls', () => {
});

it('should get taxonomy tags data with parentTag', async () => {
const taxonomyId = 123;
const taxonomyId = '123';
const options = { parentTag: 'Sample Tag' };
axiosMock.onGet().reply(200, taxonomyTagsMock);
const result = await getTaxonomyTagsData(taxonomyId, options);
Expand All @@ -63,7 +62,7 @@ describe('content tags drawer api calls', () => {
});

it('should get taxonomy tags data with page', async () => {
const taxonomyId = 123;
const taxonomyId = '123';
const options = { page: 2 };
axiosMock.onGet().reply(200, taxonomyTagsMock);
const result = await getTaxonomyTagsData(taxonomyId, options);
Expand All @@ -73,7 +72,7 @@ describe('content tags drawer api calls', () => {
});

it('should get taxonomy tags data with searchTerm', async () => {
const taxonomyId = 123;
const taxonomyId = '123';
const options = { searchTerm: 'memo' };
axiosMock.onGet().reply(200, taxonomyTagsMock);
const result = await getTaxonomyTagsData(taxonomyId, options);
Expand Down
109 changes: 109 additions & 0 deletions src/content-tags-drawer/data/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

import type { TagListData } from '@src/taxonomy/data/types';

import type { ContentData, ContentTaxonomyTagsData, UpdateTagsData } from './types';

const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;

interface GetTaxonomyTagsApiUrlOptions {
parentTag?: string;
page?: number;
searchTerm?: string;
}

/**
* Get the URL used to fetch tags data from the "taxonomy tags" REST API
*/
export const getTaxonomyTagsApiUrl = (taxonomyId: string, options: GetTaxonomyTagsApiUrlOptions = {}): string => {
const url = new URL(`api/content_tagging/v1/taxonomies/${taxonomyId}/tags/`, getApiBaseUrl());
if (options.parentTag) {
url.searchParams.append('parent_tag', options.parentTag);
}
if (options.page) {
url.searchParams.append('page', String(options.page));
}
if (options.searchTerm) {
url.searchParams.append('search_term', options.searchTerm);
}

// Load in the full tree if children at once, if we can:
// Note: do not combine this with page_size (we currently aren't using page_size)
url.searchParams.append('full_depth_threshold', '1000');

return url.href;
};

export const getContentTaxonomyTagsApiUrl = (contentId: string) => new URL(`api/content_tagging/v1/object_tags/${contentId}/`, getApiBaseUrl()).href;
export const getXBlockContentDataApiURL = (contentId: string) => new URL(`/xblock/outline/${contentId}`, getApiBaseUrl()).href;
export const getCourseContentDataApiURL = (contentId: string) => new URL(`/api/contentstore/v1/course_settings/${contentId}`, getApiBaseUrl()).href;
export const getLibraryContentDataApiUrl = (contentId: string) => new URL(`/api/libraries/v2/blocks/${contentId}/`, getApiBaseUrl()).href;
export const getContentTaxonomyTagsCountApiUrl = (contentId: string) => new URL(`api/content_tagging/v1/object_tag_counts/${contentId}/?count_implicit`, getApiBaseUrl()).href;

/**
* Get all tags that belong to taxonomy.
*/
export async function getTaxonomyTagsData(
taxonomyId: string,
options: GetTaxonomyTagsApiUrlOptions = {},
): Promise<TagListData> {
const url = getTaxonomyTagsApiUrl(taxonomyId, options);
const { data } = await getAuthenticatedHttpClient().get(url);
return camelCaseObject(data);
}

/**
* Get the tags that are applied to the content object
* @param contentId The id of the content object to fetch the applied tags for
*/
export async function getContentTaxonomyTagsData(contentId: string): Promise<ContentTaxonomyTagsData> {
const url = getContentTaxonomyTagsApiUrl(contentId);
const { data } = await getAuthenticatedHttpClient().get(url);
return camelCaseObject(data[contentId]);
}

/**
* Get the count of tags that are applied to the content object
* @param contentId The id of the content object to fetch the count of the applied tags for
*/
export async function getContentTaxonomyTagsCount(contentId: string): Promise<number> {
const url = getContentTaxonomyTagsCountApiUrl(contentId);
const { data } = await getAuthenticatedHttpClient().get(url);
if (contentId in data) {
return camelCaseObject(data[contentId]);
}
return 0;
}

/**
* Fetch meta data (eg: display_name) about the content object (unit/component)
* @param contentId The id of the content object (unit/component)
*/
export async function getContentData(contentId: string): Promise<ContentData> {
let url: string;

if (contentId.startsWith('lb:')) {
url = getLibraryContentDataApiUrl(contentId);
} else if (contentId.startsWith('course-v1:')) {
url = getCourseContentDataApiURL(contentId);
} else {
url = getXBlockContentDataApiURL(contentId);
}
const { data } = await getAuthenticatedHttpClient().get(url);
return camelCaseObject(data);
}

/**
* Update content object's applied tags
* @param contentId The id of the content object (unit/component)
* @param tagsData The list of tags (values) to set on content object
*/
export async function updateContentTaxonomyTags(
contentId: string,
tagsData: UpdateTagsData,
): Promise<ContentTaxonomyTagsData> {
const url = getContentTaxonomyTagsApiUrl(contentId);
const { data } = await getAuthenticatedHttpClient().put(url, { tagsData });
return camelCaseObject(data[contentId]);
}
2 changes: 1 addition & 1 deletion src/content-tags-drawer/data/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { TaxonomyData } from '../../taxonomy/data/types';
import type { TaxonomyData } from '@src/taxonomy/data/types';

/** A tag that has been applied to some content. */
export interface Tag {
Expand Down
1 change: 1 addition & 0 deletions src/content-tags-drawer/index.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@import "content-tags-drawer/TagsTree";
@import "content-tags-drawer/tags-sidebar-controls/TagsSidebarControls";
@import "content-tags-drawer/ContentTagsDrawer";
@import "content-tags-drawer/ContentTagsSnippet";
1 change: 1 addition & 0 deletions src/content-tags-drawer/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as ContentTagsDrawer } from './ContentTagsDrawer';
export { default as ContentTagsDrawerSheet } from './ContentTagsDrawerSheet';
export { useContentTaxonomyTagsData } from './data/apiHooks';
export { ContentTagsSnippet } from './ContentTagsSnippet';
1 change: 1 addition & 0 deletions src/course-outline/CourseOutline.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
@import "./publish-modal/PublishModal";
@import "./xblock-status/XBlockStatus";
@import "./drag-helper/SortableItem";
@import "./outline-sidebar";
Loading
Loading