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