Skip to content

Commit

Permalink
add your locked tasks widget (#2389)
Browse files Browse the repository at this point in the history
* add your locked tasks widget
  • Loading branch information
CollinBeczak authored Aug 12, 2024
1 parent 058a8aa commit 8520ba6
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/components/HOCs/WithLockedTask/WithLockedTask.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
/>
)
Expand Down
155 changes: 155 additions & 0 deletions src/components/LockedTasks/LockedTasksWidget.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className={`mr-ml-1 ${timeColor}`} title={`${minutesElapsed} minutes since: ${created}`}>
{distanceToNow}
</div>
);
};

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 ? (
<div className="mr-flex mr-flex-wrap mr-links-green-lighter">
{sortedLockedTasks.map(task => {
if (!_isFinite(_get(task, 'id'))) {
return null;
}

return (
<div key={task.id} className="mr-card-challenge mr-p-1 mr-mt-3 mr-mr-3 mr-w-full mr-flex mr-items-center" style={{maxWidth: '22rem'}}>
<div className="mr-flex mr-flex-col mr-flex-grow">
<div className="mr-flex">Started: {calculateElapsedTime(task.startedAt)}</div>
<div><FormattedMessage {...messages.taskLabel} />
<Link to={`/challenge/${task.parent}/task/${task.id}`}>
{task.id}
</Link>
</div>
<div><FormattedMessage {...messages.challengeLabel} /> <Link to={`browse/challenges/${task.parent}`}>{task.parentName}</Link></div>
</div>
<Dropdown
className="mr-dropdown--right"
dropdownButton={dropdown => (
<button
onClick={dropdown.toggleDropdownVisible}
className="mr-flex mr-items-center mr-text-green-lighter mr-mr-4"
>
<SvgSymbol
sym="locked-icon"
viewBox="0 0 20 20"
className="mr-w-4 mr-h-4 mr-fill-current"
/>
</button>
)}
dropdownContent={() => (
<div className="mr-links-green-lighter mr-text-sm mr-flex mr-items-center mr-mt-2">
<span className="mr-flex mr-items-baseline">
<FormattedMessage {...messages.taskLockedLabel} />
</span>
<button
onClick={() => {
props.unlockTask(task);
setLockedTasks(prevTasks => prevTasks.filter(prevTask => prevTask.id !== task.id));
}}
className="mr-button mr-button--xsmall mr-ml-3"
>
<FormattedMessage {...messages.unlockLabel} />
</button>
</div>
)}
/>
</div>
);
})}
</div>
) : (
<div className="mr-text-grey-lighter">
<FormattedMessage {...messages.noLockedTasks} />
</div>
);
};

const LockedTasksListComponent = WithLockedTask(LockedTasksList);

return (
<QuickWidget
{...props}
className="locked-tasks-widget"
widgetTitle={
<div>
<FormattedMessage {...messages.header} />
</div>
}
>
<FormattedMessage {...messages.description} />
<button className="mr-text-green-lighter" onClick={fetchLockedTasks}><FormattedMessage {...messages.checkList} /></button>
<LockedTasksListComponent {...props} />
</QuickWidget>
);
};

const LockedTasksWidget = WithLockedTask(LockedTasks);

registerWidgetType(LockedTasksWidget, descriptor);
export default LockedTasksWidget;
46 changes: 46 additions & 0 deletions src/components/LockedTasks/Messages.js
Original file line number Diff line number Diff line change
@@ -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",
},
})
1 change: 1 addition & 0 deletions src/components/SavedChallenges/SavedChallengesWidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ const SavedChallengeList = function(props) {
}
))


return (
challengeItems.length > 0 ?
<ol className="mr-list-reset mr-links-green-lighter mr-pb-24">
Expand Down
2 changes: 2 additions & 0 deletions src/components/Widgets/widget_registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
1 change: 1 addition & 0 deletions src/pages/Dashboard/Dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const defaultWorkspaceSetup = function() {
widgets: [
widgetDescriptor('FeaturedChallengesWidget'),
widgetDescriptor('SavedChallengesWidget'),
widgetDescriptor('LockedTasksWidget'),
widgetDescriptor('TopUserChallengesWidget'),
widgetDescriptor('UserActivityTimelineWidget'),
widgetDescriptor('PopularChallengesWidget'),
Expand Down
1 change: 1 addition & 0 deletions src/services/Server/APIRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
15 changes: 15 additions & 0 deletions src/services/User/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 8520ba6

Please sign in to comment.