Skip to content

Commit 869f7af

Browse files
committed
feat: add collection card
also fix inifinite scroll for collections
1 parent 0b42f0d commit 869f7af

File tree

5 files changed

+111
-12
lines changed

5 files changed

+111
-12
lines changed

src/generic/block-type-utils/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const STRUCTURAL_TYPE_ICONS: Record<string, React.ComponentType> = {
5151
vertical: UNIT_TYPE_ICONS_MAP.vertical,
5252
sequential: Folder,
5353
chapter: Folder,
54+
collection: Folder,
5455
};
5556

5657
export const COMPONENT_TYPE_STYLE_COLOR_MAP = {

src/library-authoring/LibraryCollections.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { CardGrid } from '@openedx/paragon';
55
import messages from './messages';
66
import { useSearchContext } from '../search-manager';
77
import { NoComponents, NoSearchResults } from './EmptyStates';
8-
import ComponentCard from './components/ComponentCard';
8+
import CollectionCard from './components/CollectionCard';
99
import { LIBRARY_SECTION_PREVIEW_LIMIT } from './components/LibrarySection';
1010

1111
type LibraryCollectionsProps = {
@@ -68,11 +68,10 @@ const LibraryCollections = ({ libraryId, variant }: LibraryCollectionsProps) =>
6868
}}
6969
hasEqualColumnHeights
7070
>
71-
{ collectionList.map((contentHit) => (
72-
<ComponentCard
73-
key={contentHit.id}
74-
contentHit={contentHit}
75-
blockTypeDisplayName={'Collection'}
71+
{ collectionList.map((collectionHit) => (
72+
<CollectionCard
73+
key={collectionHit.id}
74+
collectionHit={collectionHit}
7675
/>
7776
)) }
7877
</CardGrid>
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import React, { useContext, useMemo, useState } from 'react';
2+
import { useIntl } from '@edx/frontend-platform/i18n';
3+
import {
4+
ActionRow,
5+
Card,
6+
Container,
7+
Icon,
8+
IconButton,
9+
Dropdown,
10+
Stack,
11+
} from '@openedx/paragon';
12+
import { MoreVert } from '@openedx/paragon/icons';
13+
14+
import { getItemIcon, getComponentStyleColor } from '../../generic/block-type-utils';
15+
import { updateClipboard } from '../../generic/data/api';
16+
import TagCount from '../../generic/tag-count';
17+
import { ToastContext } from '../../generic/toast-context';
18+
import { type CollectionHit, Highlight } from '../../search-manager';
19+
import { LibraryContext } from '../common/context';
20+
import messages from './messages';
21+
import { STUDIO_CLIPBOARD_CHANNEL } from '../../constants';
22+
23+
type CollectionCardProps = {
24+
collectionHit: CollectionHit,
25+
};
26+
27+
const CollectionCard = ({ collectionHit } : CollectionCardProps) => {
28+
const intl = useIntl();
29+
30+
const {
31+
type,
32+
formatted,
33+
tags,
34+
} = collectionHit;
35+
const { displayName = '', description = '' } = formatted;
36+
37+
const tagCount = useMemo(() => {
38+
if (!tags) {
39+
return 0;
40+
}
41+
return (tags.level0?.length || 0) + (tags.level1?.length || 0)
42+
+ (tags.level2?.length || 0) + (tags.level3?.length || 0);
43+
}, [tags]);
44+
45+
const componentIcon = getItemIcon(type);
46+
47+
return (
48+
<Container className="library-component-card">
49+
<Card>
50+
<Card.Header
51+
className={`library-component-header ${getComponentStyleColor(type)}`}
52+
title={
53+
<Icon src={componentIcon} className="library-component-header-icon" />
54+
}
55+
actions={(
56+
<ActionRow>
57+
<IconButton
58+
src={MoreVert}
59+
iconAs={Icon}
60+
variant="primary"
61+
alt={intl.formatMessage(messages.collectionCardMenuAlt)}
62+
/>
63+
</ActionRow>
64+
)}
65+
/>
66+
<Card.Body>
67+
<Card.Section>
68+
<Stack direction="horizontal" className="d-flex justify-content-between">
69+
<Stack direction="horizontal" gap={1}>
70+
<Icon src={componentIcon} size="sm" />
71+
<span className="small">{intl.formatMessage(messages.collectionType)}</span>
72+
</Stack>
73+
<TagCount count={tagCount} />
74+
</Stack>
75+
<div className="text-truncate h3 mt-2">
76+
<Highlight text={displayName} />
77+
</div>
78+
<Highlight text={description} />
79+
</Card.Section>
80+
</Card.Body>
81+
</Card>
82+
</Container>
83+
);
84+
};
85+
86+
export default CollectionCard;

src/library-authoring/components/messages.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@ const messages = defineMessages({
1111
defaultMessage: 'Component actions menu',
1212
description: 'Alt/title text for the component card menu button.',
1313
},
14+
collectionCardMenuAlt: {
15+
id: 'course-authoring.library-authoring.collection.menu',
16+
defaultMessage: 'Collection actions menu',
17+
description: 'Alt/title text for the collection card menu button.',
18+
},
19+
collectionType: {
20+
id: 'course-authoring.library-authoring.collection.type',
21+
defaultMessage: 'Collection',
22+
description: 'Collection type text',
23+
},
1424
menuEdit: {
1525
id: 'course-authoring.library-authoring.component.menu.edit',
1626
defaultMessage: 'Edit',

src/search-manager/data/api.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -144,18 +144,21 @@ export interface CollectionHit {
144144
created: number;
145145
modified: number;
146146
accessId: number;
147+
/** Same fields with <mark>...</mark> highlights */
148+
formatted: { displayName: string, description: string };
147149
}
148150

149151
/**
150152
* Convert search hits to camelCase
151153
* @param hit A search result directly from Meilisearch
152154
*/
153-
function formatSearchHit(hit: Record<string, any>): ContentHit {
155+
function formatSearchHit(hit: Record<string, any>): ContentHit | CollectionHit {
154156
// eslint-disable-next-line @typescript-eslint/naming-convention
155157
const { _formatted, ...newHit } = hit;
156158
newHit.formatted = {
157159
displayName: _formatted.display_name,
158160
content: _formatted.content ?? {},
161+
description: _formatted.description,
159162
};
160163
return camelCaseObject(newHit);
161164
}
@@ -255,15 +258,15 @@ export async function fetchSearchResults({
255258
// top-level entries in the array are AND conditions and must all match
256259
// Inner arrays are OR conditions, where only one needs to match.
257260
collectionsFilter, // include only collections
258-
...typeFilters,
259261
...extraFilterFormatted,
262+
// We exclude the block type filter as collections are only of 1 type i.e. collection.
260263
...tagsFilterFormatted,
261264
],
262-
attributesToHighlight: ['display_name', 'content'],
265+
attributesToHighlight: ['display_name', 'description'],
263266
highlightPreTag: HIGHLIGHT_PRE_TAG,
264267
highlightPostTag: HIGHLIGHT_POST_TAG,
265-
attributesToCrop: ['content'],
266-
cropLength: 20,
268+
attributesToCrop: ['description'],
269+
cropLength: 15,
267270
sort,
268271
offset,
269272
limit,
@@ -275,7 +278,7 @@ export async function fetchSearchResults({
275278
totalHits: results[0].totalHits ?? results[0].estimatedTotalHits ?? results[0].hits.length,
276279
blockTypes: results[1].facetDistribution?.block_type ?? {},
277280
problemTypes: results[1].facetDistribution?.['content.problem_types'] ?? {},
278-
nextOffset: results[0].hits.length === limit ? offset + limit : undefined,
281+
nextOffset: results[0].hits.length === limit || results[2].hits.length === limit ? offset + limit : undefined,
279282
collectionHits: results[2].hits.map(formatSearchHit),
280283
totalCollectionHits: results[2].totalHits ?? results[2].estimatedTotalHits ?? results[2].hits.length,
281284
};

0 commit comments

Comments
 (0)