Skip to content

Commit 9d86cfb

Browse files
committed
refactor: create base card component
Extract common parts from component and collections card
1 parent 92cfcb2 commit 9d86cfb

File tree

5 files changed

+119
-108
lines changed

5 files changed

+119
-108
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import React, { useMemo } from 'react';
2+
import {
3+
Card,
4+
Container,
5+
Icon,
6+
Stack,
7+
} from '@openedx/paragon';
8+
import { ReactNodeLike } from 'prop-types';
9+
10+
import { getItemIcon, getComponentStyleColor } from '../../generic/block-type-utils';
11+
import TagCount from '../../generic/tag-count';
12+
import { ContentHitTags, Highlight } from '../../search-manager';
13+
14+
type BaseComponentCardProps = {
15+
type: string,
16+
displayName: string,
17+
description: string,
18+
tags: ContentHitTags,
19+
actions: ReactNodeLike,
20+
blockTypeDisplayName: string,
21+
openInfoSidebar: () => void
22+
};
23+
24+
const BaseComponentCard = ({
25+
type,
26+
displayName,
27+
description,
28+
tags,
29+
actions,
30+
blockTypeDisplayName,
31+
openInfoSidebar,
32+
} : BaseComponentCardProps) => {
33+
const tagCount = useMemo(() => {
34+
if (!tags) {
35+
return 0;
36+
}
37+
return (tags.level0?.length || 0) + (tags.level1?.length || 0)
38+
+ (tags.level2?.length || 0) + (tags.level3?.length || 0);
39+
}, [tags]);
40+
41+
const componentIcon = getItemIcon(type);
42+
43+
return (
44+
<Container className="library-component-card">
45+
<Card
46+
isClickable
47+
onClick={openInfoSidebar}
48+
onKeyDown={(e: React.KeyboardEvent) => {
49+
if (['Enter', ' '].includes(e.key)) {
50+
openInfoSidebar();
51+
}
52+
}}
53+
>
54+
<Card.Header
55+
className={`library-component-header ${getComponentStyleColor(type)}`}
56+
title={
57+
<Icon src={componentIcon} className="library-component-header-icon" />
58+
}
59+
actions={actions}
60+
/>
61+
<Card.Body>
62+
<Card.Section>
63+
<Stack direction="horizontal" className="d-flex justify-content-between">
64+
<Stack direction="horizontal" gap={1}>
65+
<Icon src={componentIcon} size="sm" />
66+
<span className="small">{blockTypeDisplayName}</span>
67+
</Stack>
68+
<TagCount count={tagCount} />
69+
</Stack>
70+
<div className="text-truncate h3 mt-2">
71+
<Highlight text={displayName} />
72+
</div>
73+
<Highlight text={description} />
74+
</Card.Section>
75+
</Card.Body>
76+
</Card>
77+
</Container>
78+
);
79+
};
80+
81+
export default BaseComponentCard;

src/library-authoring/components/CollectionCard.tsx

Lines changed: 20 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
1-
import React, { useMemo } from 'react';
21
import { useIntl } from '@edx/frontend-platform/i18n';
32
import {
43
ActionRow,
5-
Card,
6-
Container,
74
Icon,
85
IconButton,
9-
Stack,
106
} from '@openedx/paragon';
117
import { MoreVert } from '@openedx/paragon/icons';
128

13-
import { getItemIcon, getComponentStyleColor } from '../../generic/block-type-utils';
14-
import TagCount from '../../generic/tag-count';
15-
import { type CollectionHit, Highlight } from '../../search-manager';
9+
import { type CollectionHit } from '../../search-manager';
1610
import messages from './messages';
11+
import BaseComponentCard from './BaseComponentCard';
1712

1813
type CollectionCardProps = {
1914
collectionHit: CollectionHit,
@@ -29,52 +24,25 @@ const CollectionCard = ({ collectionHit } : CollectionCardProps) => {
2924
} = collectionHit;
3025
const { displayName = '', description = '' } = formatted;
3126

32-
const tagCount = useMemo(() => {
33-
if (!tags) {
34-
return 0;
35-
}
36-
return (tags.level0?.length || 0) + (tags.level1?.length || 0)
37-
+ (tags.level2?.length || 0) + (tags.level3?.length || 0);
38-
}, [tags]);
39-
40-
const componentIcon = getItemIcon(type);
41-
4227
return (
43-
<Container className="library-component-card">
44-
<Card>
45-
<Card.Header
46-
className={`library-component-header ${getComponentStyleColor(type)}`}
47-
title={
48-
<Icon src={componentIcon} className="library-component-header-icon" />
49-
}
50-
actions={(
51-
<ActionRow>
52-
<IconButton
53-
src={MoreVert}
54-
iconAs={Icon}
55-
variant="primary"
56-
alt={intl.formatMessage(messages.collectionCardMenuAlt)}
57-
/>
58-
</ActionRow>
59-
)}
60-
/>
61-
<Card.Body>
62-
<Card.Section>
63-
<Stack direction="horizontal" className="d-flex justify-content-between">
64-
<Stack direction="horizontal" gap={1}>
65-
<Icon src={componentIcon} size="sm" />
66-
<span className="small">{intl.formatMessage(messages.collectionType)}</span>
67-
</Stack>
68-
<TagCount count={tagCount} />
69-
</Stack>
70-
<div className="text-truncate h3 mt-2">
71-
<Highlight text={displayName} />
72-
</div>
73-
<Highlight text={description} />
74-
</Card.Section>
75-
</Card.Body>
76-
</Card>
77-
</Container>
28+
<BaseComponentCard
29+
type={type}
30+
displayName={displayName}
31+
description={description}
32+
tags={tags}
33+
actions={(
34+
<ActionRow>
35+
<IconButton
36+
src={MoreVert}
37+
iconAs={Icon}
38+
variant="primary"
39+
alt={intl.formatMessage(messages.collectionCardMenuAlt)}
40+
/>
41+
</ActionRow>
42+
)}
43+
blockTypeDisplayName={intl.formatMessage(messages.collectionType)}
44+
openInfoSidebar={() => {}}
45+
/>
7846
);
7947
};
8048

src/library-authoring/components/ComponentCard.tsx

Lines changed: 16 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,20 @@
1-
import React, { useContext, useMemo, useState } from 'react';
1+
import React, { useContext, useState } from 'react';
22
import { useIntl } from '@edx/frontend-platform/i18n';
33
import {
44
ActionRow,
5-
Card,
6-
Container,
75
Icon,
86
IconButton,
97
Dropdown,
10-
Stack,
118
} from '@openedx/paragon';
129
import { MoreVert } from '@openedx/paragon/icons';
1310

14-
import { getItemIcon, getComponentStyleColor } from '../../generic/block-type-utils';
1511
import { updateClipboard } from '../../generic/data/api';
16-
import TagCount from '../../generic/tag-count';
1712
import { ToastContext } from '../../generic/toast-context';
18-
import { type ContentHit, Highlight } from '../../search-manager';
13+
import { type ContentHit } from '../../search-manager';
1914
import { LibraryContext } from '../common/context';
2015
import messages from './messages';
2116
import { STUDIO_CLIPBOARD_CHANNEL } from '../../constants';
17+
import BaseComponentCard from './BaseComponentCard';
2218

2319
type ComponentCardProps = {
2420
contentHit: ContentHit,
@@ -77,55 +73,21 @@ const ComponentCard = ({ contentHit, blockTypeDisplayName } : ComponentCardProps
7773
} = contentHit;
7874
const description = formatted?.content?.htmlContent ?? '';
7975
const displayName = formatted?.displayName ?? '';
80-
const tagCount = useMemo(() => {
81-
if (!tags) {
82-
return 0;
83-
}
84-
return (tags.level0?.length || 0) + (tags.level1?.length || 0)
85-
+ (tags.level2?.length || 0) + (tags.level3?.length || 0);
86-
}, [tags]);
87-
88-
const componentIcon = getItemIcon(blockType);
8976

9077
return (
91-
<Container className="library-component-card">
92-
<Card
93-
isClickable
94-
onClick={() => openComponentInfoSidebar(usageKey)}
95-
onKeyDown={(e: React.KeyboardEvent) => {
96-
if (['Enter', ' '].includes(e.key)) {
97-
openComponentInfoSidebar(usageKey);
98-
}
99-
}}
100-
>
101-
<Card.Header
102-
className={`library-component-header ${getComponentStyleColor(blockType)}`}
103-
title={
104-
<Icon src={componentIcon} className="library-component-header-icon" />
105-
}
106-
actions={(
107-
<ActionRow>
108-
<ComponentMenu usageKey={usageKey} />
109-
</ActionRow>
110-
)}
111-
/>
112-
<Card.Body>
113-
<Card.Section>
114-
<Stack direction="horizontal" className="d-flex justify-content-between">
115-
<Stack direction="horizontal" gap={1}>
116-
<Icon src={componentIcon} size="sm" />
117-
<span className="small">{blockTypeDisplayName}</span>
118-
</Stack>
119-
<TagCount count={tagCount} />
120-
</Stack>
121-
<div className="text-truncate h3 mt-2">
122-
<Highlight text={displayName} />
123-
</div>
124-
<Highlight text={description} />
125-
</Card.Section>
126-
</Card.Body>
127-
</Card>
128-
</Container>
78+
<BaseComponentCard
79+
type={blockType}
80+
displayName={displayName}
81+
description={description}
82+
tags={tags}
83+
actions={(
84+
<ActionRow>
85+
<ComponentMenu usageKey={usageKey} />
86+
</ActionRow>
87+
)}
88+
blockTypeDisplayName={blockTypeDisplayName}
89+
openInfoSidebar={() => openComponentInfoSidebar(usageKey)}
90+
/>
12991
);
13092
};
13193

src/search-manager/data/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ function formatTagsFilter(tagsFilter?: string[]): string[] {
8383
/**
8484
* The tags that are associated with a search result, at various levels of the tag hierarchy.
8585
*/
86-
interface ContentHitTags {
86+
export interface ContentHitTags {
8787
taxonomy?: string[];
8888
level0?: string[];
8989
level1?: string[];

src/search-manager/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ export { default as SearchSortWidget } from './SearchSortWidget';
88
export { default as Stats } from './Stats';
99
export { HIGHLIGHT_PRE_TAG, HIGHLIGHT_POST_TAG } from './data/api';
1010

11-
export type { CollectionHit, ContentHit } from './data/api';
11+
export type { CollectionHit, ContentHit, ContentHitTags } from './data/api';

0 commit comments

Comments
 (0)