fix: Show tasks completion on document list items (#2342)
Co-authored-by: Tom Moor <tom.moor@gmail.com>
This commit is contained in:
74
app/components/CircularProgressBar.js
Normal file
74
app/components/CircularProgressBar.js
Normal file
@@ -0,0 +1,74 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { useTheme } from "styled-components";
|
||||
|
||||
const cleanPercentage = (percentage) => {
|
||||
const tooLow = !Number.isFinite(+percentage) || percentage < 0;
|
||||
const tooHigh = percentage > 100;
|
||||
return tooLow ? 0 : tooHigh ? 100 : +percentage;
|
||||
};
|
||||
|
||||
const Circle = ({
|
||||
color,
|
||||
percentage,
|
||||
offset,
|
||||
}: {
|
||||
color: string,
|
||||
percentage?: number,
|
||||
offset: number,
|
||||
}) => {
|
||||
const radius = offset * 0.7;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
let strokePercentage;
|
||||
if (percentage) {
|
||||
// because the circle is so small, anything greater than 85% appears like 100%
|
||||
percentage = percentage > 85 && percentage < 100 ? 85 : percentage;
|
||||
strokePercentage = percentage
|
||||
? ((100 - percentage) * circumference) / 100
|
||||
: 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<circle
|
||||
r={radius}
|
||||
cx={offset}
|
||||
cy={offset}
|
||||
fill="none"
|
||||
stroke={strokePercentage !== circumference ? color : ""}
|
||||
strokeWidth={2.5}
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={percentage ? strokePercentage : 0}
|
||||
strokeLinecap="round"
|
||||
style={{ transition: "stroke-dashoffset 0.6s ease 0s" }}
|
||||
></circle>
|
||||
);
|
||||
};
|
||||
|
||||
const CircularProgressBar = ({
|
||||
percentage,
|
||||
size = 16,
|
||||
}: {
|
||||
percentage: number,
|
||||
size?: number,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
percentage = cleanPercentage(percentage);
|
||||
const offset = Math.floor(size / 2);
|
||||
|
||||
return (
|
||||
<svg width={size} height={size}>
|
||||
<g transform={`rotate(-90 ${offset} ${offset})`}>
|
||||
<Circle color={theme.progressBarBackground} offset={offset} />
|
||||
{percentage > 0 && (
|
||||
<Circle
|
||||
color={theme.primary}
|
||||
percentage={percentage}
|
||||
offset={offset}
|
||||
/>
|
||||
)}
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default CircularProgressBar;
|
||||
@@ -6,6 +6,7 @@ import { Link } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import Document from "models/Document";
|
||||
import DocumentBreadcrumb from "components/DocumentBreadcrumb";
|
||||
import DocumentTasks from "components/DocumentTasks";
|
||||
import Flex from "components/Flex";
|
||||
import Time from "components/Time";
|
||||
import useCurrentUser from "hooks/useCurrentUser";
|
||||
@@ -64,6 +65,8 @@ function DocumentMeta({
|
||||
deletedAt,
|
||||
isDraft,
|
||||
lastViewedAt,
|
||||
isTasks,
|
||||
isTemplate,
|
||||
} = document;
|
||||
|
||||
// Prevent meta information from displaying if updatedBy is not available.
|
||||
@@ -114,6 +117,11 @@ function DocumentMeta({
|
||||
);
|
||||
}
|
||||
|
||||
const nestedDocumentsCount = collection
|
||||
? collection.getDocumentChildren(document.id).length
|
||||
: 0;
|
||||
const canShowProgressBar = isTasks && !isTemplate;
|
||||
|
||||
const timeSinceNow = () => {
|
||||
if (isDraft || !showLastViewed) {
|
||||
return null;
|
||||
@@ -133,10 +141,6 @@ function DocumentMeta({
|
||||
);
|
||||
};
|
||||
|
||||
const nestedDocumentsCount = collection
|
||||
? collection.getDocumentChildren(document.id).length
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<Container align="center" rtl={document.dir === "rtl"} {...rest} dir="ltr">
|
||||
{lastUpdatedByCurrentUser ? t("You") : updatedBy.name}
|
||||
@@ -156,6 +160,12 @@ function DocumentMeta({
|
||||
</span>
|
||||
)}
|
||||
{timeSinceNow()}
|
||||
{canShowProgressBar && (
|
||||
<>
|
||||
•
|
||||
<DocumentTasks document={document} />
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
</Container>
|
||||
);
|
||||
|
||||
33
app/components/DocumentTasks.js
Normal file
33
app/components/DocumentTasks.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CircularProgressBar from "components/CircularProgressBar";
|
||||
import Document from "../models/Document";
|
||||
|
||||
type Props = {|
|
||||
document: Document,
|
||||
|};
|
||||
|
||||
function DocumentTasks({ document }: Props) {
|
||||
const { tasks, tasksPercentage } = document;
|
||||
const { t } = useTranslation();
|
||||
const { completed, total } = tasks;
|
||||
const message =
|
||||
completed === 0
|
||||
? t(`{{ total }} tasks`, { total })
|
||||
: completed === total
|
||||
? t(`{{ completed }} tasks done`, { completed })
|
||||
: t(`{{ completed }} of {{ total }} tasks`, {
|
||||
total,
|
||||
completed,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<CircularProgressBar percentage={tasksPercentage} />
|
||||
{message}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default DocumentTasks;
|
||||
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
import { addDays, differenceInDays } from "date-fns";
|
||||
import invariant from "invariant";
|
||||
import { floor } from "lodash";
|
||||
import { action, computed, observable, set } from "mobx";
|
||||
import parseTitle from "shared/utils/parseTitle";
|
||||
import unescape from "shared/utils/unescape";
|
||||
@@ -43,6 +44,7 @@ export default class Document extends BaseModel {
|
||||
deletedAt: ?string;
|
||||
url: string;
|
||||
urlId: string;
|
||||
tasks: { completed: number, total: number };
|
||||
revision: number;
|
||||
|
||||
constructor(fields: Object, store: DocumentsStore) {
|
||||
@@ -149,6 +151,20 @@ export default class Document extends BaseModel {
|
||||
get isFromTemplate(): boolean {
|
||||
return !!this.templateId;
|
||||
}
|
||||
|
||||
@computed
|
||||
get isTasks(): boolean {
|
||||
return !!this.tasks.total;
|
||||
}
|
||||
|
||||
@computed
|
||||
get tasksPercentage(): number {
|
||||
if (!this.isTasks) {
|
||||
return 0;
|
||||
}
|
||||
return floor((this.tasks.completed / this.tasks.total) * 100);
|
||||
}
|
||||
|
||||
@action
|
||||
share = async () => {
|
||||
return this.store.rootStore.shares.create({ documentId: this.id });
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Prompt, Route, withRouter } from "react-router-dom";
|
||||
import type { RouterHistory, Match } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import getTasks from "shared/utils/getTasks";
|
||||
import AuthStore from "stores/AuthStore";
|
||||
import ToastsStore from "stores/ToastsStore";
|
||||
import UiStore from "stores/UiStore";
|
||||
@@ -223,6 +224,8 @@ class DocumentScene extends React.Component<Props> {
|
||||
this.isSaving = true;
|
||||
this.isPublishing = !!options.publish;
|
||||
|
||||
document.tasks = getTasks(document.text);
|
||||
|
||||
try {
|
||||
const savedDocument = await document.save({
|
||||
...options,
|
||||
|
||||
Reference in New Issue
Block a user