Skip to content

Commit ba2cbe7

Browse files
committed
Add TopTagSuggestions component and integrate it into EditTask, TaskConfirmationModal, and TaskTags
- Introduced TopTagSuggestions component to display popular tags for challenges. - Integrated TopTagSuggestions into EditTask, TaskConfirmationModal, and TaskTags for enhanced tag management. - Updated API routes to fetch top tags for challenges. - Added loading and message handling for the TopTagSuggestions component.
1 parent 00977a3 commit ba2cbe7

8 files changed

Lines changed: 161 additions & 7 deletions

File tree

src/components/AdminPane/Manage/ManageTasks/EditTask/EditTask.jsx

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
} from "../../../../Custom/RJSFFormFieldAdapter/RJSFFormFieldAdapter";
1818
import WithTaskTags from "../../../../HOCs/WithTaskTags/WithTaskTags";
1919
import KeywordAutosuggestInput from "../../../../KeywordAutosuggestInput/KeywordAutosuggestInput";
20+
import TopTagSuggestions from "../../../../TopTagSuggestions";
2021
import WithCurrentChallenge from "../../../HOCs/WithCurrentChallenge/WithCurrentChallenge";
2122
import WithCurrentProject from "../../../HOCs/WithCurrentProject/WithCurrentProject";
2223
import WithCurrentTask from "../../../HOCs/WithCurrentTask/WithCurrentTask";
@@ -128,13 +129,27 @@ export class EditTask extends Component {
128129
);
129130

130131
return (
131-
<KeywordAutosuggestInput
132-
{...props}
133-
inputClassName="mr-p-2 mr-border-2 mr-border-grey-light-more mr-text-grey mr-rounded"
134-
tagType={"tasks"}
135-
preferredResults={preferredTags}
136-
placeholder={this.props.intl.formatMessage(messages.addTagsPlaceholder)}
137-
/>
132+
<div>
133+
<KeywordAutosuggestInput
134+
{...props}
135+
inputClassName="mr-p-2 mr-border-2 mr-border-grey-light-more mr-text-grey mr-rounded"
136+
tagType={"tasks"}
137+
preferredResults={preferredTags}
138+
placeholder={this.props.intl.formatMessage(messages.addTagsPlaceholder)}
139+
/>
140+
141+
{/* Show top tag suggestions from challenge */}
142+
{this.props.challenge && (
143+
<TopTagSuggestions
144+
challengeId={this.props.challenge.id}
145+
currentTags={props.formData}
146+
onAddTag={(tag) => {
147+
const currentTags = props.formData || "";
148+
props.onChange(currentTags ? `${currentTags},${tag}` : tag);
149+
}}
150+
/>
151+
)}
152+
</div>
138153
);
139154
},
140155
};

src/components/TaskConfirmationModal/TaskConfirmationModal.jsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import Modal from "../Modal/Modal";
3636
import TaskCommentInput from "../TaskCommentInput/TaskCommentInput";
3737
import TaskNearbyList from "../TaskPane/TaskNearbyList/TaskNearbyList";
3838
import TaskReviewNearbyList from "../TaskPane/TaskNearbyList/TaskReviewNearbyList";
39+
import TopTagSuggestions from "../TopTagSuggestions";
3940
import AdjustFiltersOverlay from "./AdjustFiltersOverlay";
4041
import InstructionsOverlay from "./InstructionsOverlay";
4142
import messages from "./Messages";
@@ -372,6 +373,15 @@ export class TaskConfirmationModal extends Component {
372373
placeholder={this.props.intl.formatMessage(messages.addTagsPlaceholder)}
373374
/>
374375

376+
{/* Show top tag suggestions from challenge */}
377+
{this.props.task && this.props.task.parent && (
378+
<TopTagSuggestions
379+
challengeId={this.props.task.parent.id}
380+
currentTags={this.props.tags}
381+
onAddTag={this.handleAddTag}
382+
/>
383+
)}
384+
375385
{this.props.submitComment && (
376386
<div className="mr-my-1 mr-flex mr-justify-end">
377387
<button

src/components/TaskTags/TaskTags.jsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import External from "../External/External";
88
import KeywordAutosuggestInput from "../KeywordAutosuggestInput/KeywordAutosuggestInput";
99
import Modal from "../Modal/Modal";
1010
import SvgSymbol from "../SvgSymbol/SvgSymbol";
11+
import TopTagSuggestions from "../TopTagSuggestions";
1112
import messages from "./Messages";
1213

1314
export class TaskTags extends Component {
@@ -96,6 +97,15 @@ export class TaskTags extends Component {
9697
limitToPreferred={limitTags}
9798
placeholder={this.props.intl.formatMessage(messages.addTagsPlaceholder)}
9899
/>
100+
101+
{/* Show top tag suggestions from challenge */}
102+
{this.props.task && this.props.task.parent && (
103+
<TopTagSuggestions
104+
challengeId={this.props.task.parent.id}
105+
currentTags={this.props.tags}
106+
onAddTag={this.handleAddTag}
107+
/>
108+
)}
99109
</div>
100110
<div className="mr-flex mr-justify-end mr-items-center mr-mt-8">
101111
<button
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { defineMessages } from "react-intl";
2+
3+
/**
4+
* Internationalized messages for use with TopTagSuggestions
5+
*/
6+
export default defineMessages({
7+
loading: {
8+
id: "TopTags.loading",
9+
defaultMessage: "Loading popular tags...",
10+
},
11+
topTagsLabel: {
12+
id: "TopTags.label",
13+
defaultMessage: "Popular tags on this challenge:",
14+
},
15+
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import TopTagSuggestions from "./index.jsx";
2+
export default TopTagSuggestions;
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import _isEmpty from "lodash/isEmpty";
2+
import { useEffect, useState } from "react";
3+
import { FormattedMessage } from "react-intl";
4+
import { fetchTopTags } from "../../services/Challenge/TopTags";
5+
import messages from "./Messages";
6+
7+
/**
8+
* TopTagSuggestions displays the most popular tags for a challenge
9+
* and allows users to quickly add them to their task
10+
*
11+
* @param {Object} props - Component props
12+
* @param {number} props.challengeId - The id of the challenge to fetch top tags for
13+
* @param {string} props.currentTags - Comma-separated string of current tags
14+
* @param {Function} props.onAddTag - Callback function when a tag is clicked
15+
*/
16+
const TopTagSuggestions = (props) => {
17+
const [loading, setLoading] = useState(true);
18+
const [tags, setTags] = useState([]);
19+
20+
useEffect(() => {
21+
if (props.challengeId) {
22+
setLoading(true);
23+
fetchTopTags(props.challengeId)
24+
.then((topTags) => {
25+
if (topTags) {
26+
setTags(topTags);
27+
}
28+
setLoading(false);
29+
})
30+
.catch(() => setLoading(false));
31+
}
32+
}, [props.challengeId]);
33+
34+
// Don't render if there are no tags
35+
if (!loading && _isEmpty(tags)) {
36+
return null;
37+
}
38+
39+
// Parse current tags to avoid duplicates
40+
const currentTagsArray = props.currentTags ? props.currentTags.split(/,\s*/) : [];
41+
42+
return (
43+
<div className="mr-mt-4">
44+
{loading ? (
45+
<span className="mr-text-sm mr-text-grey-light">
46+
<FormattedMessage {...messages.loading} />
47+
</span>
48+
) : (
49+
<>
50+
<div className="mr-text-sm mr-text-grey-light mr-mb-1">
51+
<FormattedMessage {...messages.topTagsLabel} />
52+
</div>
53+
<div className="mr-flex mr-flex-wrap">
54+
{tags.map((tag) => {
55+
const isAlreadyAdded = currentTagsArray.includes(tag.name);
56+
57+
return (
58+
<button
59+
key={tag.id}
60+
className={`mr-button mr-button--small mr-py-1 mr-text-xs mr-mr-2 mr-mb-2 ${
61+
isAlreadyAdded ? "mr-button--disabled" : ""
62+
}`}
63+
onClick={() => !isAlreadyAdded && props.onAddTag(tag.name)}
64+
disabled={isAlreadyAdded}
65+
title={isAlreadyAdded ? "Already added" : `Add tag: ${tag.name}`}
66+
>
67+
<span>{tag.name}</span>
68+
</button>
69+
);
70+
})}
71+
</div>
72+
</>
73+
)}
74+
</div>
75+
);
76+
};
77+
78+
export default TopTagSuggestions;

src/services/Challenge/TopTags.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import _values from "lodash/values";
2+
import _map from "lodash/map";
3+
import Endpoint from "../Server/Endpoint";
4+
import { defaultRoutes as api } from "../Server/Server";
5+
6+
/**
7+
* Fetch top tags for a challenge
8+
*/
9+
export const fetchTopTags = function (challengeId) {
10+
return new Endpoint(api.challenge.topTags, {
11+
schema: {},
12+
variables: { id: challengeId },
13+
})
14+
.execute()
15+
.then((response) => {
16+
// The API returns an object with numeric keys, convert to array
17+
return response?.result ? Object.values(response.result) : [];
18+
})
19+
.catch((error) => {
20+
console.log(error.response || error);
21+
return [];
22+
});
23+
};

src/services/Server/APIRoutes.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ const apiRoutes = (factory) => {
8585
removeSnapshot: factory.delete("/snapshot/:id"),
8686
snapshot: factory.get("/snapshot/:id"),
8787
archive: factory.post("/challenge/:id/archive"),
88+
topTags: factory.get("/challenge/:id/topTags"),
8889
},
8990
virtualChallenge: {
9091
single: factory.get("/virtualchallenge/:id"),

0 commit comments

Comments
 (0)