Merge branch 'main' of github.com:outline/outline

This commit is contained in:
Tom Moor
2021-07-27 10:24:36 -04:00
17 changed files with 293 additions and 75 deletions

View File

@@ -94,6 +94,10 @@ FORCE_HTTPS=true
# the maintainers
ENABLE_UPDATES=true
# How many processes should be spawned. As a reasonable rule divide your servers
# available memory by 512 for a rough estimate
WEB_CONCURRENCY=1
# Override the maxium size of document imports, could be required if you have
# especially large Word documents with embedded imagery
MAXIMUM_IMPORT_SIZE=5120000

View 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;

View File

@@ -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}&nbsp;
@@ -156,6 +160,12 @@ function DocumentMeta({
</span>
)}
&nbsp;{timeSinceNow()}
{canShowProgressBar && (
<>
&nbsp;&nbsp;
<DocumentTasks document={document} />
</>
)}
{children}
</Container>
);

View 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} />
&nbsp;{message}
</>
);
}
export default DocumentTasks;

View File

@@ -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 });

View File

@@ -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,

View File

@@ -58,7 +58,6 @@
"babel-plugin-styled-components": "^1.11.1",
"babel-plugin-transform-class-properties": "^6.24.1",
"boundless-arrow-key-navigation": "^1.0.4",
"boxen": "^5.0.1",
"bull": "^3.5.2",
"cancan": "3.1.0",
"chalk": "^4.1.0",
@@ -160,6 +159,7 @@
"styled-components": "^5.2.3",
"styled-components-breakpoint": "^2.1.1",
"styled-normalize": "^8.0.4",
"throng": "^5.0.0",
"tiny-cookie": "^2.3.1",
"tmp": "^0.2.1",
"turndown": "^7.1.1",

View File

@@ -2,8 +2,8 @@
require("dotenv").config({ silent: true });
const errors = [];
const boxen = require("boxen");
const chalk = require("chalk");
const throng = require("throng");
// If the DataDog agent is installed and the DD_API_KEY environment variable is
// in the environment then we can safely attempt to start the DD tracer
@@ -66,7 +66,7 @@ if (!process.env.URL) {
);
}
if (!process.env.DATABASE_URL) {
if (!process.env.DATABASE_URL && !process.env.DATABASE_CONNECTION_POOL_URL) {
errors.push(
`The ${chalk.bold(
"DATABASE_URL"
@@ -95,11 +95,10 @@ if (errors.length) {
if (process.env.NODE_ENV === "production") {
console.log(
boxen(
chalk.green(
`
Is your team enjoying Outline? Consider supporting future development by sponsoring the project:\n\nhttps://github.com/sponsors/outline
`,
{ padding: 1, margin: 1, borderStyle: "double", borderColor: "green" }
`
)
);
} else if (process.env.NODE_ENV === "development") {
@@ -112,4 +111,11 @@ Is your team enjoying Outline? Consider supporting future development by sponsor
);
}
require("./main");
const { start } = require("./main");
throng({
worker: start,
// The number of workers to run, defaults to the number of CPUs available
count: process.env.WEB_CONCURRENCY || undefined,
});

View File

@@ -234,10 +234,12 @@ server.on("listening", () => {
console.log(`\n> Listening on http://localhost:${address.port}\n`);
});
(async () => {
export async function start(id: string) {
console.log(`Started worker ${id}`);
await checkMigrations();
server.listen(process.env.PORT || "3000");
})();
}
export const socketio = io;

View File

@@ -6,6 +6,7 @@ import Sequelize, { Transaction } from "sequelize";
import MarkdownSerializer from "slate-md-serializer";
import isUUID from "validator/lib/isUUID";
import { MAX_TITLE_LENGTH } from "../../shared/constants";
import getTasks from "../../shared/utils/getTasks";
import parseTitle from "../../shared/utils/parseTitle";
import { SLUG_URL_REGEX } from "../../shared/utils/routeHelpers";
import unescape from "../../shared/utils/unescape";
@@ -106,6 +107,9 @@ const Document = sequelize.define(
const slugifiedTitle = slugify(this.title);
return `/doc/${slugifiedTitle}-${this.urlId}`;
},
tasks: function () {
return getTasks(this.text || "");
},
},
}
);

View File

@@ -430,3 +430,79 @@ describe("#findByPk", () => {
expect(response.id).toBe(document.id);
});
});
describe("tasks", () => {
test("should consider all the possible checkTtems", async () => {
const document = await buildDocument({
text: `- [x] test
- [X] test
- [ ] test
- [-] test
- [_] test`,
});
const tasks = document.tasks;
expect(tasks.completed).toBe(4);
expect(tasks.total).toBe(5);
});
test("should return tasks keys set to 0 if checkItems isn't present", async () => {
const document = await buildDocument({
text: `text`,
});
const tasks = document.tasks;
expect(tasks.completed).toBe(0);
expect(tasks.total).toBe(0);
});
test("should return tasks keys set to 0 if the text contains broken checkItems", async () => {
const document = await buildDocument({
text: `- [x ] test
- [ x ] test
- [ ] test`,
});
const tasks = document.tasks;
expect(tasks.completed).toBe(0);
expect(tasks.total).toBe(0);
});
test("should return tasks", async () => {
const document = await buildDocument({
text: `- [x] list item
- [ ] list item`,
});
const tasks = document.tasks;
expect(tasks.completed).toBe(1);
expect(tasks.total).toBe(2);
});
test("should update tasks on save", async () => {
const document = await buildDocument({
text: `- [x] list item
- [ ] list item`,
});
const tasks = document.tasks;
expect(tasks.completed).toBe(1);
expect(tasks.total).toBe(2);
document.text = `- [x] list item
- [ ] list item
- [ ] list item`;
await document.save();
const newTasks = document.tasks;
expect(newTasks.completed).toBe(1);
expect(newTasks.total).toBe(3);
});
});

View File

@@ -44,6 +44,7 @@ export default async function present(document: Document, options: ?Options) {
title: document.title,
text,
emoji: document.emoji,
tasks: document.tasks,
createdAt: document.createdAt,
createdBy: undefined,
updatedAt: document.updatedAt,

View File

@@ -12,16 +12,19 @@ export const encryptedFields = () =>
export const DataTypes = Sequelize;
export const Op = Sequelize.Op;
export const sequelize = new Sequelize(process.env.DATABASE_URL, {
logging: debug("sql"),
typeValidation: true,
dialectOptions: {
ssl:
isProduction && !isSSLDisabled
? {
// Ref.: https://github.com/brianc/node-postgres/issues/2009
rejectUnauthorized: false,
}
: false,
},
});
export const sequelize = new Sequelize(
process.env.DATABASE_URL || process.env.DATABASE_CONNECTION_POOL_URL,
{
logging: debug("sql"),
typeValidation: true,
dialectOptions: {
ssl:
isProduction && !isSSLDisabled
? {
// Ref.: https://github.com/brianc/node-postgres/issues/2009
rejectUnauthorized: false,
}
: false,
},
}
);

View File

@@ -34,6 +34,9 @@
"only you": "only you",
"person": "person",
"people": "people",
"{{ total }} tasks": "{{ total }} tasks",
"{{ completed }} tasks done": "{{ completed }} tasks done",
"{{ completed }} of {{ total }} tasks": "{{ completed }} of {{ total }} tasks",
"Currently editing": "Currently editing",
"Currently viewing": "Currently viewing",
"Viewed {{ timeAgo }} ago": "Viewed {{ timeAgo }} ago",

View File

@@ -179,6 +179,7 @@ export const light = {
noticeInfoBackground: colors.warmGrey,
noticeInfoText: colors.almostBlack,
progressBarBackground: colors.slateLight,
scrollbarBackground: colors.smoke,
scrollbarThumb: darken(0.15, colors.smokeDark),
@@ -241,6 +242,7 @@ export const dark = {
noticeInfoBackground: colors.white10,
noticeInfoText: colors.almostWhite,
progressBarBackground: colors.slate,
scrollbarBackground: colors.black,
scrollbarThumb: colors.lightBlack,

21
shared/utils/getTasks.js Normal file
View File

@@ -0,0 +1,21 @@
// @flow
const CHECKBOX_REGEX = /\[(X|\s|_|-)\]\s(.*)?/gi;
export default function getTasks(text: string) {
const matches = [...text.matchAll(CHECKBOX_REGEX)];
let total = matches.length;
if (!total) {
return {
completed: 0,
total: 0,
};
} else {
const notCompleted = matches.reduce(
(accumulator, match) =>
match[1] === " " ? accumulator + 1 : accumulator,
0
);
return { completed: total - notCompleted, total };
}
}

View File

@@ -2490,13 +2490,6 @@ ansi-align@^2.0.0:
dependencies:
string-width "^2.0.0"
ansi-align@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.0.tgz#b536b371cf687caaef236c18d3e21fe3797467cb"
integrity sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==
dependencies:
string-width "^3.0.0"
ansi-colors@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
@@ -3253,20 +3246,6 @@ boxen@^1.2.1:
term-size "^1.2.0"
widest-line "^2.0.0"
boxen@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.0.1.tgz#657528bdd3f59a772b8279b831f27ec2c744664b"
integrity sha512-49VBlw+PrWEF51aCmy7QIteYPIFZxSpvqBdP/2itCPPlJ49kj9zg/XPRFrdkne2W+CfwXUls8exMvu1RysZpKA==
dependencies:
ansi-align "^3.0.0"
camelcase "^6.2.0"
chalk "^4.1.0"
cli-boxes "^2.2.1"
string-width "^4.2.0"
type-fest "^0.20.2"
widest-line "^3.1.0"
wrap-ansi "^7.0.0"
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@@ -3627,7 +3606,7 @@ camelcase@^5.0.0, camelcase@^5.3.1:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
camelcase@^6.0.0, camelcase@^6.2.0:
camelcase@^6.0.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809"
integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
@@ -3858,11 +3837,6 @@ cli-boxes@^1.0.0:
resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143"
integrity sha1-T6kXw+WclKAEzWH47lCdplFocUM=
cli-boxes@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f"
integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==
cli-color@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/cli-color/-/cli-color-1.4.0.tgz#7d10738f48526824f8fe7da51857cb0f572fe01f"
@@ -12525,7 +12499,7 @@ string-width@^3.0.0, string-width@^3.1.0:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^5.1.0"
string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0:
string-width@^4.1.0, string-width@^4.2.0:
version "4.2.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5"
integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==
@@ -12896,6 +12870,13 @@ throat@^5.0.0:
resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b"
integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==
throng@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/throng/-/throng-5.0.0.tgz#f9550c0221e579073f68a00be33a593d094e4d29"
integrity sha512-nrq7+qQhn/DL8yW/wiwImTepfi6ynOCAe7moSwgoYN1F32yQMdBkuFII40oAkb3cDfaL6q5BIoFTDCHdMWQ8Pw==
dependencies:
lodash "^4.17.20"
through2-filter@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254"
@@ -13214,11 +13195,6 @@ type-fest@^0.16.0:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.16.0.tgz#3240b891a78b0deae910dbeb86553e552a148860"
integrity sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==
type-fest@^0.20.2:
version "0.20.2"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
type-fest@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
@@ -14010,13 +13986,6 @@ widest-line@^2.0.0:
dependencies:
string-width "^2.1.1"
widest-line@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca"
integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==
dependencies:
string-width "^4.0.0"
windows-release@^3.1.0:
version "3.3.3"
resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-3.3.3.tgz#1c10027c7225743eec6b89df160d64c2e0293999"
@@ -14228,15 +14197,6 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"