From 8520ba6569d47b809c22b21d8faefe1c55e2c905 Mon Sep 17 00:00:00 2001
From: Collin Beczak <88843144+CollinBeczak@users.noreply.github.com>
Date: Mon, 12 Aug 2024 09:21:14 -0500
Subject: [PATCH] add your locked tasks widget (#2389)
* add your locked tasks widget
---
.../HOCs/WithLockedTask/WithLockedTask.js | 1 +
.../LockedTasks/LockedTasksWidget.js | 155 ++++++++++++++++++
src/components/LockedTasks/Messages.js | 46 ++++++
.../SavedChallenges/SavedChallengesWidget.js | 1 +
src/components/Widgets/widget_registry.js | 2 +
src/pages/Dashboard/Dashboard.js | 1 +
src/services/Server/APIRoutes.js | 1 +
src/services/User/User.js | 15 ++
8 files changed, 222 insertions(+)
create mode 100644 src/components/LockedTasks/LockedTasksWidget.js
create mode 100644 src/components/LockedTasks/Messages.js
diff --git a/src/components/HOCs/WithLockedTask/WithLockedTask.js b/src/components/HOCs/WithLockedTask/WithLockedTask.js
index 097fd2596..760c93a69 100644
--- a/src/components/HOCs/WithLockedTask/WithLockedTask.js
+++ b/src/components/HOCs/WithLockedTask/WithLockedTask.js
@@ -152,6 +152,7 @@ const WithLockedTask = function(WrappedComponent) {
tryingLock={this.state.tryingLock}
lockFailureDetails={this.state.failureDetails}
tryLocking={this.lockTask}
+ unlockTask={this.unlockTask}
refreshTaskLock={this.refreshTaskLock}
/>
)
diff --git a/src/components/LockedTasks/LockedTasksWidget.js b/src/components/LockedTasks/LockedTasksWidget.js
new file mode 100644
index 000000000..d0289b1e1
--- /dev/null
+++ b/src/components/LockedTasks/LockedTasksWidget.js
@@ -0,0 +1,155 @@
+import React, { useEffect, useState } from 'react';
+import { FormattedMessage } from 'react-intl';
+import _get from 'lodash/get';
+import _isFinite from 'lodash/isFinite';
+import { WidgetDataTarget, registerWidgetType } from '../../services/Widget/Widget';
+import { fetchUsersLockedTasks } from '../../services/User/User';
+import { Link } from 'react-router-dom';
+import messages from './Messages';
+import QuickWidget from '../QuickWidget/QuickWidget';
+import SvgSymbol from '../SvgSymbol/SvgSymbol';
+import Dropdown from '../Dropdown/Dropdown';
+import WithLockedTask from '../HOCs/WithLockedTask/WithLockedTask';
+import { differenceInMinutes, formatDistanceToNow, parseISO } from 'date-fns';
+
+const descriptor = {
+ widgetKey: 'LockedTasksWidget',
+ label: messages.header,
+ targets: [
+ WidgetDataTarget.user,
+ ],
+ minWidth: 3,
+ defaultWidth: 4,
+ minHeight: 2,
+ defaultHeight: 5,
+};
+
+const LockedTasks = (props) => {
+ const [lockedTasks, setLockedTasks] = useState([]);
+ const [currentTime, setCurrentTime] = useState(new Date());
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setCurrentTime(new Date());
+ }, 60000); // Update every minute
+
+ return () => clearInterval(intervalId);
+ }, []);
+
+ const calculateElapsedTime = (startDate) => {
+ const timestamp = parseISO(startDate);
+ const created = `${props.intl.formatDate(timestamp)} ${props.intl.formatTime(timestamp)}`;
+ const distanceToNow = formatDistanceToNow(timestamp, { addSuffix: true });
+ const minutesElapsed = differenceInMinutes(currentTime, timestamp);
+
+ let timeColor = '';
+ if (minutesElapsed > 60) {
+ timeColor = 'mr-text-red';
+ } else if (minutesElapsed > 30) {
+ timeColor = 'mr-text-orange';
+ }
+
+ return (
+
+ {distanceToNow}
+
+ );
+ };
+
+ const fetchLockedTasks = async () => {
+ if (props.user) {
+ const tasks = await fetchUsersLockedTasks(props.user.id);
+ setLockedTasks(tasks);
+ }
+ };
+
+ useEffect(() => {
+ fetchLockedTasks();
+ }, [props.user]);
+
+ const LockedTasksList = () => {
+ const sortedLockedTasks = [...lockedTasks].sort((a, b) => new Date(a.startedAt) - new Date(b.startedAt));
+
+ return sortedLockedTasks.length > 0 ? (
+
+ {sortedLockedTasks.map(task => {
+ if (!_isFinite(_get(task, 'id'))) {
+ return null;
+ }
+
+ return (
+
+
+
Started: {calculateElapsedTime(task.startedAt)}
+
+
+ {task.id}
+
+
+
{task.parentName}
+
+
(
+
+ )}
+ dropdownContent={() => (
+
+
+
+
+
+
+ )}
+ />
+
+ );
+ })}
+
+ ) : (
+
+
+
+ );
+ };
+
+ const LockedTasksListComponent = WithLockedTask(LockedTasksList);
+
+ return (
+
+
+
+ }
+ >
+
+
+
+
+ );
+};
+
+const LockedTasksWidget = WithLockedTask(LockedTasks);
+
+registerWidgetType(LockedTasksWidget, descriptor);
+export default LockedTasksWidget;
diff --git a/src/components/LockedTasks/Messages.js b/src/components/LockedTasks/Messages.js
new file mode 100644
index 000000000..534c6fa13
--- /dev/null
+++ b/src/components/LockedTasks/Messages.js
@@ -0,0 +1,46 @@
+import { defineMessages } from 'react-intl'
+
+/**
+ * Internationalized messages for use with SavedChallenges.
+ */
+export default defineMessages({
+ header: {
+ id: "UserProfile.lockedTasks.header",
+ defaultMessage: "Your Locked Tasks",
+ },
+
+ noLockedTasks: {
+ id: "SavedChallenges.widget.noTasks",
+ defaultMessage: "You have no locked tasks",
+ },
+
+ taskLabel: {
+ id: "Admin.Task.fields.name.label",
+ defaultMessage: "Task:",
+ },
+
+ challengeLabel: {
+ id: 'TaskConfirmationModal.challenge.label',
+ defaultMessage: "Challenge:",
+ },
+
+ description: {
+ id: "SavedChallenges.widget.description",
+ defaultMessage: "Tasks locked for more than an hour will be automatically unlocked within the next hour or might already be unlocked. ",
+ },
+
+ checkList: {
+ id: "SavedChallenges.widget.checkList.label",
+ defaultMessage: "Refresh list to check.",
+ },
+
+ unlockLabel: {
+ id: "Task.pane.controls.unlock.label",
+ defaultMessage: "You have no locked tasks",
+ },
+
+ taskLockedLabel: {
+ id: "ReviewTaskPane.indicators.locked.label",
+ defaultMessage: "Task locked",
+ },
+})
diff --git a/src/components/SavedChallenges/SavedChallengesWidget.js b/src/components/SavedChallenges/SavedChallengesWidget.js
index 9ffad25cb..cf3d8090f 100644
--- a/src/components/SavedChallenges/SavedChallengesWidget.js
+++ b/src/components/SavedChallenges/SavedChallengesWidget.js
@@ -109,6 +109,7 @@ const SavedChallengeList = function(props) {
}
))
+
return (
challengeItems.length > 0 ?
diff --git a/src/components/Widgets/widget_registry.js b/src/components/Widgets/widget_registry.js
index 2caddfd97..888ce7503 100644
--- a/src/components/Widgets/widget_registry.js
+++ b/src/components/Widgets/widget_registry.js
@@ -57,6 +57,8 @@ export { default as TopUserChallengesWidget }
from '../TopUserChallenges/TopUserChallengesWidget'
export { default as SavedChallengesWidget }
from '../SavedChallenges/SavedChallengesWidget'
+export { default as LockedTasksWidget }
+ from '../LockedTasks/LockedTasksWidget'
export { default as FeaturedChallengesWidget }
from '../FeaturedChallenges/FeaturedChallengesWidget'
export { default as PopularChallengesWidget }
diff --git a/src/pages/Dashboard/Dashboard.js b/src/pages/Dashboard/Dashboard.js
index 4d8ca5cd0..c1b4c9679 100644
--- a/src/pages/Dashboard/Dashboard.js
+++ b/src/pages/Dashboard/Dashboard.js
@@ -23,6 +23,7 @@ export const defaultWorkspaceSetup = function() {
widgets: [
widgetDescriptor('FeaturedChallengesWidget'),
widgetDescriptor('SavedChallengesWidget'),
+ widgetDescriptor('LockedTasksWidget'),
widgetDescriptor('TopUserChallengesWidget'),
widgetDescriptor('UserActivityTimelineWidget'),
widgetDescriptor('PopularChallengesWidget'),
diff --git a/src/services/Server/APIRoutes.js b/src/services/Server/APIRoutes.js
index df9da7e3a..7df257bfb 100644
--- a/src/services/Server/APIRoutes.js
+++ b/src/services/Server/APIRoutes.js
@@ -179,6 +179,7 @@ const apiRoutes = (factory) => {
unsaveChallenge: factory.delete("/user/:userId/unsave/:challengeId"),
savedTasks: factory.get("/user/:userId/savedTasks"),
saveTask: factory.post("/user/:userId/saveTask/:taskId"),
+ lockedTasks: factory.get("/user/:userId/lockedTasks"),
unsaveTask: factory.delete("/user/:userId/unsaveTask/:taskId"),
updateSettings: factory.put("/user/:userId"),
notificationSubscriptions: factory.get(
diff --git a/src/services/User/User.js b/src/services/User/User.js
index bdb7bc3dd..7b2b49ec3 100644
--- a/src/services/User/User.js
+++ b/src/services/User/User.js
@@ -418,6 +418,21 @@ export const fetchSavedChallenges = function(userId, limit=50) {
}
}
+/**
+ * Fetch the saved challenges for the given user.
+ */
+export const fetchUsersLockedTasks = async (userId, limit=50) => {
+ return new Endpoint(
+ api.user.lockedTasks, {
+ variables: {userId},
+ params: {limit}
+ }
+ ).execute().then(normalizedChallenges => {
+ return normalizedChallenges
+ })
+}
+
+
/**
* Fetch the user's top challengs based on recent activity beginning at
* the given startDate. If no date is given, then activity over the past