Compare commits

...

52 Commits

Author SHA1 Message Date
Tom Moor d4c594423f More email styling 2021-07-08 22:38:42 -04:00
Tom Moor 2bf237d54b Merge branch 'main' into email-diff 2021-07-08 21:20:18 -04:00
Tom Moor 9a1c8f07d1 feat: Add documentId filter to events.list (#2287) 2021-07-08 10:12:06 -07:00
Tom Moor 241cb11493 chore: Automate running yarn-deduplicate, see comment:
https://github.com/outline/outline/pull/2283\#discussion_r665301770
2021-07-07 22:26:56 -04:00
Saumya Pandey 8195791bb2 fix: Make search query string user friendly (#2283)
* Upgrade query-string package and skip empty string

* Run yarn-deduplicate command
2021-07-07 18:45:40 -07:00
Saumya Pandey b037ae5dc1 fix: Improve isChildDocument performance (#2284) 2021-07-07 04:53:40 -07:00
Tom Moor aeba8ce4eb fix: Empty context menu when user does not have permission to update collection 2021-07-06 22:02:31 -04:00
Tom Moor 3565e68725 Merge branch 'main' into email-diff 2021-07-06 20:43:51 -04:00
Tom Moor 429c5fba85 0.57.0 2021-07-06 09:12:54 -04:00
Tom Moor 9495ddba25 fix: Restore previous WSS CORS behavior 2021-07-05 23:01:25 -04:00
dependabot[bot] 486a60e97c chore(deps): bump socket.io from 2.3.0 to 2.4.0 (#1831)
Bumps [socket.io](https://github.com/socketio/socket.io) from 2.3.0 to 2.4.0.
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/2.4.0/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/2.3.0...2.4.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-05 17:11:40 -07:00
Tom Moor c687745263 fix: E-mail signin on incorrect subdomain should allow the process to continue instead of error
closes #2276
2021-07-05 19:25:21 -04:00
Translate-O-Tron 1b92993b90 fix: New Portuguese, Brazilian translations from Crowdin (#2271) 2021-07-05 13:37:09 -07:00
Tom Moor 181a20a268 fix: More context menu fixes 2021-07-05 16:35:46 -04:00
Tom Moor f8ffa4e25a tweak sidebar item background 2021-07-04 18:44:09 -04:00
Farzad 7e139ca8f7 fix: nested link positions for RTL titles in sidebar (#2272) 2021-07-04 12:08:05 -07:00
Tom Moor bb58db507d fix: ew-resize -> col-resize cursor 2021-07-04 11:54:29 -04:00
Translate-O-Tron 49bf86d6d9 New Crowdin updates (#2268) 2021-07-04 06:54:52 -07:00
Tom Moor 286a15cf10 fix: Clicking dropdown menu items in FF (#2269)
* fix: Clicking dropdown menu items in FF
closes #2264

* fix: Anchor items, add comment

* fix: CI test memory issues
2021-07-04 06:54:40 -07:00
Tom Moor f65469b777 lockfile 2021-07-03 21:22:52 -04:00
Translate-O-Tron fe65a79d66 New Crowdin updates (#2267) 2021-07-03 07:02:01 -07:00
Tom Moor a1d5ac0907 RTL document support (#2263)
* Basic RTL support in documents

* fix: DocumentListItem and ReferenceListItem for RTL content
2021-07-03 07:00:10 -07:00
Tom Moor 04eabe68a7 feat: Enable traditional Chinese translations (#2266) 2021-07-02 12:08:08 -07:00
Tom Moor 1c0c694c22 fix: Email auth should allow same guest user on multiple subdomains (#2252)
* test: Add email auth tests to establish current state of system

* fix: Update logic to account for dupe emails used between subdomains

* test

* test
2021-07-02 12:07:43 -07:00
Translate-O-Tron 2ae74f2834 New Crowdin updates (#2262) 2021-07-02 11:23:02 -07:00
Tom Moor 0f01fc5faa test: Reduce memory usage by not requiring stores into all (#2265) 2021-07-02 11:16:07 -07:00
Tom Moor 7f1322b7ba fix: Down arrow in search input should move focus to results (#2257)
closes #2253
2021-07-01 15:01:30 -07:00
dependabot[bot] 3c98133e24 chore(deps): bump socket.io-parser from 3.3.1 to 3.3.2 (#2258)
Bumps [socket.io-parser](https://github.com/socketio/socket.io-parser) from 3.3.1 to 3.3.2.
- [Release notes](https://github.com/socketio/socket.io-parser/releases)
- [Changelog](https://github.com/socketio/socket.io-parser/blob/3.3.2/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io-parser/compare/3.3.1...3.3.2)

---
updated-dependencies:
- dependency-name: socket.io-parser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-01 12:08:57 -07:00
Tom Moor 088353d61f fix: Data loading state not reset when props change to PaginatedList (#2254)
* fix: Data loading state not reset when significant props change to PaginatedList

closes #2251

* test: Add enzyme and component test
2021-06-26 21:49:25 -07:00
Translate-O-Tron 31180619e1 New Crowdin updates (#2182) 2021-06-26 13:43:54 -07:00
Saumya Pandey 9fccc280d7 fix: Add ability to permanently delete documents in trash (#2192)
* Align false conditions before true

* Update documents.delete endpoint for permanent delete

* Add permanent delete to events table and integrate with socket.io

* Add permanent delete to document menu

* Update parentDocumentId of direct child to null

* Add translation

* Add test for permanent delete

* Add space

* Update app/scenes/DocumentPermanentDelete.js

Co-authored-by: Tom Moor <tom.moor@gmail.com>

* Update app/stores/DocumentsStore.js

Co-authored-by: Tom Moor <tom.moor@gmail.com>

* Update server/commands/documentPermanentDeleter.js

Co-authored-by: Tom Moor <tom.moor@gmail.com>

* Update app/scenes/DocumentPermanentDelete.js

Co-authored-by: Tom Moor <tom.moor@gmail.com>

* Change socket room from team to collection

* Add translation

* Create log func for commands

* Move tests from utils to permanentDeleter command

* Add additional tests

* Set redirect to trash

* Return promise from beforeEach

* Add undeleted documents validation

* Include deleteAt attribute in db query

* Update server/commands/documentPermanentDeleter.js

Co-authored-by: Tom Moor <tom.moor@gmail.com>

* tweak language

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2021-06-25 16:14:40 -07:00
Tom Moor c69b4efc34 fix: Aligned images do not load in publicly shared documents (#2248) 2021-06-25 10:09:44 -07:00
Tom Moor 61039e9d0d Allow images in email diff 2021-06-25 09:41:34 -07:00
Tom Moor 6d09122d56 test: Deletion 2021-06-24 20:10:42 -07:00
Tom Moor 5fb6097153 Improved diff 2021-06-23 23:58:32 -07:00
Tom Moor ec17874568 Remove test harness 2021-06-22 07:35:38 -07:00
Tom Moor 40c3e9e85f test 2021-06-22 07:27:55 -07:00
Tom Moor 9f739f3788 Merge main 2021-06-22 07:26:45 -07:00
Tom Moor 3cec6b4903 fix: Allow for offline development 2021-06-21 21:40:28 -07:00
Tom Moor ede7f2e3e6 fix: Bump RME (table and image fixes) 2021-06-21 17:39:14 -07:00
Tom Moor f6837b4742 wip 2021-06-20 23:15:04 -07:00
Tom Moor 1560e3c9f7 refactor 2021-06-20 12:49:15 -07:00
Tom Moor ca74908dc5 test 2021-06-20 00:20:37 -07:00
Tom Moor de7ec1119b Integrate into mailer, basic styling 2021-06-19 23:50:36 -07:00
Tom Moor 2093b4297f Merge main 2021-06-19 17:05:19 -07:00
Tom Moor cf8fa5ffa3 fix: Bump RME (checkbox list fixes) 2021-06-18 16:28:27 -07:00
Tom Moor 1a2a0f4264 fix: Long search term causes server error writing query to db (#2237)
closes #2234
2021-06-17 23:23:35 -07:00
Tom Moor 5f3a38bf87 fix: todo list checkbox consistency issue
closes #2179
2021-06-17 22:57:55 -07:00
Tom Moor afff3a6f25 fix: Server error when user cancels OAuth process with Azure (#2231) 2021-06-16 21:45:20 -07:00
Tom Moor b5824879a3 Merge branch 'fix/concat-tags' 2021-06-16 18:36:28 -07:00
Tom Moor 1c82e292e0 fix: Allow embed of private mindmeister embeds
fix: Missing right and bottom border of some embeds
2021-06-16 18:36:21 -07:00
Nan Yu 3df82c500b wip 2021-02-21 11:52:00 -08:00
92 changed files with 3729 additions and 930 deletions
+30
View File
@@ -0,0 +1,30 @@
{
"testURL": "http://localhost",
"verbose": false,
"rootDir": "..",
"roots": [
"<rootDir>/app",
"<rootDir>/shared"
],
"moduleNameMapper": {
"^shared/(.*)$": "<rootDir>/shared/$1",
"^.*[.](gif|ttf|eot|svg)$": "<rootDir>/__test__/fileMock.js"
},
"moduleFileExtensions": [
"js",
"jsx",
"json"
],
"moduleDirectories": [
"node_modules"
],
"modulePaths": [
"<rootDir>/app"
],
"setupFiles": [
"<rootDir>/__mocks__/window.js"
],
"setupFilesAfterEnv": [
"./app/test/setup.js"
]
}
+13 -1
View File
@@ -29,15 +29,26 @@ const MenuItem = ({
const handleClick = React.useCallback(
(ev) => {
if (onClick) {
ev.preventDefault();
ev.stopPropagation();
onClick(ev);
}
if (hide) {
hide();
}
},
[hide, onClick]
[onClick, hide]
);
// Preventing default mousedown otherwise menu items do not work in Firefox,
// which triggers the hideOnClickOutside handler first via mousedown hiding
// and un-rendering the menu contents.
const handleMouseDown = React.useCallback((ev) => {
ev.preventDefault();
ev.stopPropagation();
}, []);
return (
<BaseMenuItem
onClick={disabled ? undefined : onClick}
@@ -51,6 +62,7 @@ const MenuItem = ({
$toggleable={selected !== undefined}
as={onClick ? "button" : as}
onClick={handleClick}
onMouseDown={handleMouseDown}
>
{selected !== undefined && (
<>
+6 -2
View File
@@ -83,7 +83,7 @@ const Submenu = React.forwardRef(({ templateItems, title, ...rest }, ref) => {
);
});
function Template({ items, ...menu }: Props): React.Node {
export function filterTemplateItems(items: TMenuItem[]): TMenuItem[] {
let filtered = items.filter((item) => item.visible !== false);
// this block literally just trims unneccessary separators
@@ -101,7 +101,11 @@ function Template({ items, ...menu }: Props): React.Node {
return [...acc, item];
}, []);
return filtered.map((item, index) => {
return filtered;
}
function Template({ items, ...menu }: Props): React.Node {
return filterTemplateItems(items).map((item, index) => {
if (item.to) {
return (
<MenuItem
+1 -1
View File
@@ -46,7 +46,7 @@ export default function ContextMenu({
<Menu hideOnClickOutside preventBodyScroll {...rest}>
{(props) => (
<Position {...props}>
<Background>
<Background dir="auto">
{rest.visible || rest.animating ? children : null}
</Background>
</Position>
+11 -4
View File
@@ -41,7 +41,7 @@ function replaceResultMarks(tag: string) {
return tag.replace(/<b\b[^>]*>(.*?)<\/b>/gi, "$1");
}
function DocumentListItem(props: Props) {
function DocumentListItem(props: Props, ref) {
const { t } = useTranslation();
const { policies } = useStores();
const currentUser = useCurrentUser();
@@ -68,6 +68,8 @@ function DocumentListItem(props: Props) {
return (
<DocumentLink
ref={ref}
dir={document.dir}
$isStarred={document.isStarred}
$menuOpen={menuOpen}
to={{
@@ -76,8 +78,12 @@ function DocumentListItem(props: Props) {
}}
>
<Content>
<Heading>
<Title text={document.titleWithDefault} highlight={highlight} />
<Heading dir={document.dir}>
<Title
text={document.titleWithDefault}
highlight={highlight}
dir={document.dir}
/>
{document.isNew && document.createdBy.id !== currentUser.id && (
<Badge yellow>{t("New")}</Badge>
)}
@@ -221,6 +227,7 @@ const DocumentLink = styled(Link)`
const Heading = styled.h3`
display: flex;
justify-content: ${(props) => (props.rtl ? "flex-end" : "flex-start")};
align-items: center;
height: 24px;
margin-top: 0;
@@ -251,4 +258,4 @@ const ResultContext = styled(Highlight)`
margin-bottom: 0.25em;
`;
export default observer(DocumentListItem);
export default observer(React.forwardRef(DocumentListItem));
+2 -1
View File
@@ -11,6 +11,7 @@ import Time from "components/Time";
import useStores from "hooks/useStores";
const Container = styled(Flex)`
justify-content: ${(props) => (props.rtl ? "flex-end" : "flex-start")};
color: ${(props) => props.theme.textTertiary};
font-size: 13px;
white-space: nowrap;
@@ -135,7 +136,7 @@ function DocumentMeta({
: 0;
return (
<Container align="center" {...rest}>
<Container align="center" rtl={document.dir === "rtl"} {...rest} dir="ltr">
{updatedByMe ? t("You") : updatedBy.name}&nbsp;
{to ? <Link to={to}>{content}</Link> : content}
{showCollection && collection && (
+2
View File
@@ -14,6 +14,7 @@ type Props = {|
document: Document,
isDraft: boolean,
to?: string,
rtl?: boolean,
|};
function DocumentMetaWithViews({ to, isDraft, document, ...rest }: Props) {
@@ -62,6 +63,7 @@ function DocumentMetaWithViews({ to, isDraft, document, ...rest }: Props) {
}
const Meta = styled(DocumentMeta)`
justify-content: ${(props) => (props.rtl ? "flex-end" : "flex-start")};
margin: -12px 0 2em 0;
font-size: 14px;
position: relative;
+14 -1
View File
@@ -1,6 +1,18 @@
// @flow
import { format, formatDistanceToNow } from "date-fns";
import { enUS, de, fr, es, it, ko, ptBR, pt, zhCN, ru } from "date-fns/locale";
import {
enUS,
de,
fr,
es,
it,
ko,
ptBR,
pt,
zhCN,
zhTW,
ru,
} from "date-fns/locale";
import * as React from "react";
import Tooltip from "components/Tooltip";
import useUserLocale from "hooks/useUserLocale";
@@ -15,6 +27,7 @@ const locales = {
pt_BR: ptBR,
pt_PT: pt,
zh_CN: zhCN,
zh_TW: zhTW,
ru_RU: ru,
};
+14 -4
View File
@@ -38,14 +38,24 @@ class PaginatedList extends React.Component<Props> {
}
componentDidUpdate(prevProps: Props) {
if (prevProps.fetch !== this.props.fetch) {
this.fetchResults();
}
if (!isEqual(prevProps.options, this.props.options)) {
if (
prevProps.fetch !== this.props.fetch ||
!isEqual(prevProps.options, this.props.options)
) {
this.reset();
this.fetchResults();
}
}
reset = () => {
this.offset = 0;
this.allowLoadMore = true;
this.renderCount = DEFAULT_PAGINATION_LIMIT;
this.isFetching = false;
this.isFetchingMore = false;
this.isLoaded = false;
};
fetchResults = async () => {
if (!this.props.fetch) return;
+84
View File
@@ -0,0 +1,84 @@
// @flow
import "../stores";
import { shallow } from "enzyme";
import * as React from "react";
import { DEFAULT_PAGINATION_LIMIT } from "stores/BaseStore";
import { runAllPromises } from "../test/support";
import PaginatedList from "./PaginatedList";
describe("PaginatedList", () => {
const render = () => null;
it("with no items renders nothing", () => {
const list = shallow(<PaginatedList items={[]} renderItem={render} />);
expect(list).toEqual({});
});
it("with no items renders empty prop", () => {
const list = shallow(
<PaginatedList
items={[]}
empty={<p>Sorry, no results</p>}
renderItem={render}
/>
);
expect(list.text()).toEqual("Sorry, no results");
});
it("calls fetch with options + pagination on mount", () => {
const fetch = jest.fn();
const options = { id: "one" };
shallow(
<PaginatedList
items={[]}
fetch={fetch}
options={options}
renderItem={render}
/>
);
expect(fetch).toHaveBeenCalledWith({
...options,
limit: DEFAULT_PAGINATION_LIMIT,
offset: 0,
});
});
it("calls fetch when options prop changes", async () => {
const fetchedItems = Array(DEFAULT_PAGINATION_LIMIT).fill();
const fetch = jest.fn().mockReturnValue(fetchedItems);
const list = shallow(
<PaginatedList
items={[]}
fetch={fetch}
options={{ id: "one" }}
renderItem={render}
/>
);
await runAllPromises();
expect(fetch).toHaveBeenCalledWith({
id: "one",
limit: DEFAULT_PAGINATION_LIMIT,
offset: 0,
});
fetch.mockReset();
list.setProps({
fetch,
items: [],
options: { id: "two" },
});
await runAllPromises();
expect(fetch).toHaveBeenCalledWith({
id: "two",
limit: DEFAULT_PAGINATION_LIMIT,
offset: 0,
});
});
});
@@ -27,16 +27,19 @@ type Props = {|
parentId?: string,
|};
function DocumentLink({
node,
canUpdate,
collection,
activeDocument,
prefetchDocument,
depth,
index,
parentId,
}: Props) {
function DocumentLink(
{
node,
canUpdate,
collection,
activeDocument,
prefetchDocument,
depth,
index,
parentId,
}: Props,
ref
) {
const { documents, policies } = useStores();
const { t } = useTranslation();
@@ -236,6 +239,7 @@ function DocumentLink({
depth={depth}
exact={false}
showActions={menuOpen}
ref={ref}
menu={
document && !isMoving ? (
<Fade>
@@ -289,5 +293,6 @@ const Disclosure = styled(CollapsedIcon)`
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
`;
const ObservedDocumentLink = observer(DocumentLink);
const ObservedDocumentLink = observer(React.forwardRef(DocumentLink));
export default ObservedDocumentLink;
@@ -65,6 +65,7 @@ function EditableTitle({ title, onSubmit, canUpdate }: Props) {
{isEditing ? (
<form onSubmit={handleSave}>
<Input
dir="auto"
type="text"
value={value}
onKeyDown={handleKeyDown}
@@ -7,7 +7,7 @@ const ResizeBorder = styled.div`
bottom: 0;
right: -6px;
width: 12px;
cursor: ew-resize;
cursor: col-resize;
`;
export default ResizeBorder;
@@ -1,4 +1,5 @@
// @flow
import { transparentize } from "polished";
import * as React from "react";
import { withRouter, type RouterHistory, type Match } from "react-router-dom";
import styled, { withTheme } from "styled-components";
@@ -29,25 +30,28 @@ type Props = {
depth?: number,
};
function SidebarLink({
icon,
children,
onClick,
onMouseEnter,
to,
label,
active,
isActiveDrop,
menu,
showActions,
theme,
exact,
href,
depth,
history,
match,
className,
}: Props) {
function SidebarLink(
{
icon,
children,
onClick,
onMouseEnter,
to,
label,
active,
isActiveDrop,
menu,
showActions,
theme,
exact,
href,
depth,
history,
match,
className,
}: Props,
ref
) {
const style = React.useMemo(() => {
return {
paddingLeft: `${(depth || 0) * 16 + 16}px`,
@@ -78,6 +82,7 @@ function SidebarLink({
as={to ? undefined : href ? "a" : "div"}
href={href}
className={className}
ref={ref}
>
{icon && <IconWrapper>{icon}</IconWrapper>}
<Label>{label}</Label>
@@ -141,7 +146,8 @@ const Link = styled(NavLink)`
&:focus {
color: ${(props) => props.theme.text};
background: ${(props) => props.theme.black05};
background: ${(props) =>
transparentize("0.25", props.theme.sidebarItemBackground)};
}
${breakpoint("tablet")`
@@ -172,6 +178,9 @@ const Label = styled.div`
width: 100%;
max-height: 4.8em;
line-height: 1.6;
* {
unicode-bidi: plaintext;
}
`;
export default withRouter(withTheme(SidebarLink));
export default withRouter(withTheme(React.forwardRef(SidebarLink)));
+4
View File
@@ -250,6 +250,10 @@ class SocketProvider extends React.Component<Props> {
documents.starredIds.set(event.documentId, false);
});
this.socket.on("documents.permanent_delete", (event) => {
documents.remove(event.documentId);
});
// received when a user is given access to a collection
// if the user is us then we go ahead and load the collection from API.
this.socket.on("collections.add_user", (event) => {
+4 -1
View File
@@ -17,7 +17,10 @@ export default class Mindmeister extends React.Component<Props> {
static ENABLED = [URL_REGEX];
render() {
const chartId = this.props.attrs.matches[4] + this.props.attrs.matches[6];
const chartId =
this.props.attrs.matches[4] +
(this.props.attrs.matches[5] || "") +
(this.props.attrs.matches[6] || "");
return (
<Frame
+9 -9
View File
@@ -11,9 +11,7 @@ import Flex from "components/Flex";
const Iframe = (props) => <iframe title="Embed" {...props} />;
const StyledIframe = styled(Iframe)`
border: 1px solid;
border-color: ${(props) => props.theme.embedBorder};
border-radius: ${(props) => (props.withBar ? "3px 3px 0 0" : "3px")};
border-radius: ${(props) => (props.$withBar ? "3px 3px 0 0" : "3px")};
display: block;
`;
@@ -70,13 +68,13 @@ class Frame extends React.Component<PropsWithRef> {
<Rounded
width={width}
height={height}
withBar={withBar}
$withBar={withBar}
className={isSelected ? "ProseMirror-selectednode" : ""}
>
{this.isLoaded && (
<Component
ref={forwardedRef}
withBar={withBar}
$withBar={withBar}
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
width={width}
height={height}
@@ -108,10 +106,11 @@ class Frame extends React.Component<PropsWithRef> {
}
const Rounded = styled.div`
border-radius: ${(props) => (props.withBar ? "3px 3px 0 0" : "3px")};
border: 1px solid ${(props) => props.theme.embedBorder};
border-radius: 6px;
overflow: hidden;
width: ${(props) => props.width};
height: ${(props) => (props.withBar ? props.height + 28 : props.height)};
height: ${(props) => (props.$withBar ? props.height + 28 : props.height)};
`;
const Open = styled.a`
@@ -132,11 +131,12 @@ const Title = styled.span`
`;
const Bar = styled(Flex)`
border-top: 1px solid ${(props) => props.theme.embedBorder};
background: ${(props) => props.theme.secondaryBackground};
color: ${(props) => props.theme.textSecondary};
padding: 0 8px;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
user-select: none;
`;
+48 -42
View File
@@ -12,7 +12,7 @@ import CollectionExport from "scenes/CollectionExport";
import CollectionPermissions from "scenes/CollectionPermissions";
import ContextMenu from "components/ContextMenu";
import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
import Template from "components/ContextMenu/Template";
import Template, { filterTemplateItems } from "components/ContextMenu/Template";
import Modal from "components/Modal";
import useStores from "hooks/useStores";
import getDataTransferFiles from "utils/getDataTransferFiles";
@@ -110,6 +110,52 @@ function CollectionMenu({
);
const can = policies.abilities(collection.id);
const items = React.useMemo(
() =>
filterTemplateItems([
{
title: t("New document"),
visible: can.update,
onClick: handleNewDocument,
},
{
title: t("Import document"),
visible: can.update,
onClick: handleImportDocument,
},
{
type: "separator",
},
{
title: `${t("Edit")}`,
visible: can.update,
onClick: () => setShowCollectionEdit(true),
},
{
title: `${t("Permissions")}`,
visible: can.update,
onClick: () => setShowCollectionPermissions(true),
},
{
title: `${t("Export")}`,
visible: !!(collection && can.export),
onClick: () => setShowCollectionExport(true),
},
{
type: "separator",
},
{
title: `${t("Delete")}`,
visible: !!(collection && can.delete),
onClick: () => setShowCollectionDelete(true),
},
]),
[can, collection, handleNewDocument, handleImportDocument, t]
);
if (!items.length) {
return null;
}
return (
<>
@@ -134,47 +180,7 @@ function CollectionMenu({
onClose={onClose}
aria-label={t("Collection")}
>
<Template
{...menu}
items={[
{
title: t("New document"),
visible: can.update,
onClick: handleNewDocument,
},
{
title: t("Import document"),
visible: can.update,
onClick: handleImportDocument,
},
{
type: "separator",
},
{
title: `${t("Edit")}`,
visible: can.update,
onClick: () => setShowCollectionEdit(true),
},
{
title: `${t("Permissions")}`,
visible: can.update,
onClick: () => setShowCollectionPermissions(true),
},
{
title: `${t("Export")}`,
visible: !!(collection && can.export),
onClick: () => setShowCollectionExport(true),
},
{
type: "separator",
},
{
title: `${t("Delete")}`,
visible: !!(collection && can.delete),
onClick: () => setShowCollectionDelete(true),
},
]}
/>
<Template {...menu} items={items} />
</ContextMenu>
{renderModals && (
<>
+63 -33
View File
@@ -9,6 +9,7 @@ import styled from "styled-components";
import Document from "models/Document";
import DocumentDelete from "scenes/DocumentDelete";
import DocumentMove from "scenes/DocumentMove";
import DocumentPermanentDelete from "scenes/DocumentPermanentDelete";
import DocumentTemplatize from "scenes/DocumentTemplatize";
import CollectionIcon from "components/CollectionIcon";
import ContextMenu from "components/ContextMenu";
@@ -61,6 +62,10 @@ function DocumentMenu({
const { t } = useTranslation();
const [renderModals, setRenderModals] = React.useState(false);
const [showDeleteModal, setShowDeleteModal] = React.useState(false);
const [
showPermanentDeleteModal,
setShowPermanentDeleteModal,
] = React.useState(false);
const [showMoveModal, setShowMoveModal] = React.useState(false);
const [showTemplateModal, setShowTemplateModal] = React.useState(false);
const file = React.useRef<?HTMLInputElement>();
@@ -327,6 +332,11 @@ function DocumentMenu({
onClick: () => setShowDeleteModal(true),
visible: !!can.delete,
},
{
title: `${t("Permanently delete")}`,
onClick: () => setShowPermanentDeleteModal(true),
visible: can.permanentDelete,
},
{
title: `${t("Move")}`,
onClick: () => setShowMoveModal(true),
@@ -357,40 +367,60 @@ function DocumentMenu({
</ContextMenu>
{renderModals && (
<>
<Modal
title={t("Move {{ documentName }}", {
documentName: document.noun,
})}
onRequestClose={() => setShowMoveModal(false)}
isOpen={showMoveModal}
>
<DocumentMove
document={document}
{can.move && (
<Modal
title={t("Move {{ documentName }}", {
documentName: document.noun,
})}
onRequestClose={() => setShowMoveModal(false)}
/>
</Modal>
<Modal
title={t("Delete {{ documentName }}", {
documentName: document.noun,
})}
onRequestClose={() => setShowDeleteModal(false)}
isOpen={showDeleteModal}
>
<DocumentDelete
document={document}
onSubmit={() => setShowDeleteModal(false)}
/>
</Modal>
<Modal
title={t("Create template")}
onRequestClose={() => setShowTemplateModal(false)}
isOpen={showTemplateModal}
>
<DocumentTemplatize
document={document}
onSubmit={() => setShowTemplateModal(false)}
/>
</Modal>
isOpen={showMoveModal}
>
<DocumentMove
document={document}
onRequestClose={() => setShowMoveModal(false)}
/>
</Modal>
)}
{can.delete && (
<Modal
title={t("Delete {{ documentName }}", {
documentName: document.noun,
})}
onRequestClose={() => setShowDeleteModal(false)}
isOpen={showDeleteModal}
>
<DocumentDelete
document={document}
onSubmit={() => setShowDeleteModal(false)}
/>
</Modal>
)}
{can.permanentDelete && (
<Modal
title={t("Permanently delete {{ documentName }}", {
documentName: document.noun,
})}
onRequestClose={() => setShowPermanentDeleteModal(false)}
isOpen={showPermanentDeleteModal}
>
<DocumentPermanentDelete
document={document}
onSubmit={() => setShowPermanentDeleteModal(false)}
/>
</Modal>
)}
{can.update && (
<Modal
title={t("Create template")}
onRequestClose={() => setShowTemplateModal(false)}
isOpen={showTemplateModal}
>
<DocumentTemplatize
document={document}
onSubmit={() => setShowTemplateModal(false)}
/>
</Modal>
)}
</>
)}
</>
+20
View File
@@ -58,6 +58,26 @@ export default class Document extends BaseModel {
return emoji;
}
/**
* Best-guess the text direction of the document based on the language the
* title is written in. Note: wrapping as a computed getter means that it will
* only be called directly when the title changes.
*/
@computed
get dir(): "rtl" | "ltr" {
const element = document.createElement("p");
element.innerHTML = this.title;
element.style.visibility = "hidden";
element.dir = "auto";
// element must appear in body for direction to be computed
document.body?.appendChild(element);
const direction = window.getComputedStyle(element).direction;
document.body?.removeChild(element);
return direction;
}
@computed
get noun(): string {
return this.template ? "template" : "document";
+19 -17
View File
@@ -149,25 +149,27 @@ function CollectionScene() {
/>
</Action>
{can.update && (
<Action>
<Tooltip
tooltip={t("New document")}
shortcut="n"
delay={500}
placement="bottom"
>
<Button
as={Link}
to={collection ? newDocumentUrl(collection.id) : ""}
disabled={!collection}
icon={<PlusIcon />}
<>
<Action>
<Tooltip
tooltip={t("New document")}
shortcut="n"
delay={500}
placement="bottom"
>
{t("New doc")}
</Button>
</Tooltip>
</Action>
<Button
as={Link}
to={collection ? newDocumentUrl(collection.id) : ""}
disabled={!collection}
icon={<PlusIcon />}
>
{t("New doc")}
</Button>
</Tooltip>
</Action>
<Separator />
</>
)}
<Separator />
<Action>
<CollectionMenu
collection={collection}
+10
View File
@@ -33,6 +33,7 @@ type Props = {|
@observer
class DocumentEditor extends React.Component<Props> {
@observable activeLinkEvent: ?MouseEvent;
ref = React.createRef<HTMLDivElement | HTMLInputElement>();
focusAtStart = () => {
if (this.props.innerRef.current) {
@@ -114,8 +115,10 @@ class DocumentEditor extends React.Component<Props> {
{readOnly ? (
<Title
as="div"
ref={this.ref}
$startsWithEmojiAndSpace={startsWithEmojiAndSpace}
$isStarred={document.isStarred}
dir="auto"
>
<span>{normalizedTitle}</span>{" "}
{!shareId && <StarButton document={document} size={32} />}
@@ -123,6 +126,7 @@ class DocumentEditor extends React.Component<Props> {
) : (
<Title
type="text"
ref={this.ref}
onChange={onChangeTitle}
onKeyDown={this.handleTitleKeyDown}
placeholder={document.placeholder}
@@ -130,6 +134,7 @@ class DocumentEditor extends React.Component<Props> {
$startsWithEmojiAndSpace={startsWithEmojiAndSpace}
autoFocus={!title}
maxLength={MAX_TITLE_LENGTH}
dir="auto"
/>
)}
{!shareId && (
@@ -137,6 +142,11 @@ class DocumentEditor extends React.Component<Props> {
isDraft={isDraft}
document={document}
to={documentHistoryUrl(document)}
rtl={
this.ref.current
? window.getComputedStyle(this.ref.current).direction === "rtl"
: false
}
/>
)}
<Editor
@@ -35,7 +35,6 @@ const DocumentLink = styled(Link)`
const Title = styled.h3`
display: flex;
align-items: center;
max-width: 90%;
overflow: hidden;
text-overflow: ellipsis;
font-size: 14px;
@@ -78,7 +77,7 @@ function ReferenceListItem({
}}
{...rest}
>
<Title>
<Title dir="auto">
{document.emoji ? (
<Emoji>{document.emoji}</Emoji>
) : (
+60
View File
@@ -0,0 +1,60 @@
// @flow
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import { useHistory } from "react-router-dom";
import Document from "models/Document.js";
import Button from "components/Button";
import Flex from "components/Flex";
import HelpText from "components/HelpText";
import useStores from "hooks/useStores";
type Props = {|
document: Document,
onSubmit: () => void,
|};
function DocumentPermanentDelete({ document, onSubmit }: Props) {
const [isDeleting, setIsDeleting] = React.useState(false);
const { t } = useTranslation();
const { ui, documents } = useStores();
const { showToast } = ui;
const history = useHistory();
const handleSubmit = React.useCallback(
async (ev: SyntheticEvent<>) => {
ev.preventDefault();
try {
setIsDeleting(true);
await documents.delete(document, { permanent: true });
showToast(t("Document permanently deleted"), { type: "success" });
onSubmit();
history.push("/trash");
} catch (err) {
showToast(err.message, { type: "error" });
} finally {
setIsDeleting(false);
}
},
[document, onSubmit, showToast, t, history, documents]
);
return (
<Flex column>
<form onSubmit={handleSubmit}>
<HelpText>
<Trans
defaults="Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone."
values={{ documentTitle: document.titleWithDefault }}
components={{ em: <strong /> }}
/>
</HelpText>
<Button type="submit" danger>
{isDeleting ? `${t("Deleting")}` : t("Im sure  Delete")}
</Button>
</form>
</Flex>
);
}
export default observer(DocumentPermanentDelete);
+7 -4
View File
@@ -50,10 +50,13 @@ class Drafts extends React.Component<Props> {
}) => {
this.props.history.replace({
pathname: this.props.location.pathname,
search: queryString.stringify({
...queryString.parse(this.props.location.search),
...search,
}),
search: queryString.stringify(
{
...queryString.parse(this.props.location.search),
...search,
},
{ skipEmptyString: true }
),
});
};
+10 -7
View File
@@ -6,7 +6,6 @@ import { observer, inject } from "mobx-react";
import { PlusIcon } from "outline-icons";
import queryString from "query-string";
import * as React from "react";
import ReactDOM from "react-dom";
import { withTranslation, Trans, type TFunction } from "react-i18next";
import keydown from "react-keydown";
import { withRouter, Link } from "react-router-dom";
@@ -103,8 +102,9 @@ class Search extends React.Component<Props> {
if (ev.key === "ArrowDown") {
ev.preventDefault();
if (this.firstDocument) {
const element = ReactDOM.findDOMNode(this.firstDocument);
if (element instanceof HTMLElement) element.focus();
if (this.firstDocument instanceof HTMLElement) {
this.firstDocument.focus();
}
}
}
};
@@ -140,10 +140,13 @@ class Search extends React.Component<Props> {
}) => {
this.props.history.replace({
pathname: this.props.location.pathname,
search: queryString.stringify({
...queryString.parse(this.props.location.search),
...search,
}),
search: queryString.stringify(
{
...queryString.parse(this.props.location.search),
...search,
},
{ skipEmptyString: true }
),
});
};
+2 -2
View File
@@ -616,8 +616,8 @@ export default class DocumentsStore extends BaseStore<Document> {
}
@action
async delete(document: Document) {
await super.delete(document);
async delete(document: Document, options?: {| permanent: boolean |}) {
await super.delete(document, options);
// check to see if we have any shares related to this document already
// loaded in local state. If so we can go ahead and remove those too.
+9
View File
@@ -0,0 +1,9 @@
// @flow
/* eslint-disable */
import localStorage from '../../__mocks__/localStorage';
import Enzyme from "enzyme";
import Adapter from "enzyme-adapter-react-16";
Enzyme.configure({ adapter: new Adapter() });
global.localStorage = localStorage;
+2
View File
@@ -0,0 +1,2 @@
// @flow
export const runAllPromises = () => new Promise<void>(setImmediate);
+14 -35
View File
@@ -13,6 +13,7 @@
"dev": "nodemon --exec \"yarn build:server && yarn build:i18n && node --inspect=0.0.0.0 build/server/index.js\" -e js --ignore build/ --ignore app/",
"lint": "eslint app server shared",
"deploy": "git push heroku master",
"postinstall": "yarn yarn-deduplicate yarn.lock",
"heroku-postbuild": "yarn build && yarn db:migrate",
"sequelize:migrate": "sequelize db:migrate",
"db:create-migration": "sequelize migration:create",
@@ -20,7 +21,7 @@
"db:rollback": "sequelize db:migrate:undo",
"upgrade": "git fetch && git pull && yarn install && yarn heroku-postbuild",
"test": "yarn test:app && yarn test:server",
"test:app": "jest",
"test:app": "jest --config=app/.jestconfig.json --runInBand --forceExit",
"test:server": "jest --config=server/.jestconfig.json --runInBand --forceExit",
"test:watch": "jest --config=server/.jestconfig.json --runInBand --forceExit --watchAll"
},
@@ -28,33 +29,6 @@
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/outline"
},
"jest": {
"testURL": "http://localhost",
"verbose": false,
"roots": [
"app",
"shared"
],
"moduleNameMapper": {
"^shared/(.*)$": "<rootDir>/shared/$1",
"^.*[.](gif|ttf|eot|svg)$": "<rootDir>/__test__/fileMock.js"
},
"moduleFileExtensions": [
"js",
"jsx",
"json"
],
"moduleDirectories": [
"node_modules"
],
"modulePaths": [
"app"
],
"setupFiles": [
"<rootDir>/setupJest.js",
"<rootDir>/__mocks__/window.js"
]
},
"engines": {
"node": ">= 12 <=16"
},
@@ -136,6 +110,7 @@
"mobx": "^4.15.4",
"mobx-react": "^6.3.1",
"natural-sort": "^1.0.0",
"node-htmldiff": "^0.9.3",
"nodemailer": "^6.4.16",
"outline-icons": "^1.27.0",
"oy-vey": "^0.10.0",
@@ -145,12 +120,12 @@
"pg": "^8.5.1",
"pg-hstore": "^2.3.3",
"polished": "3.6.5",
"query-string": "^4.3.4",
"query-string": "^7.0.1",
"quoted-printable": "^1.0.1",
"randomstring": "1.1.5",
"raw-loader": "^0.5.1",
"react": "^17.0.2",
"react-autosize-textarea": "^6.0.0",
"react-autosize-textarea": "^7.1.0",
"react-avatar-editor": "^11.1.0",
"react-color": "^2.17.3",
"react-dnd": "^14.0.1",
@@ -167,9 +142,9 @@
"react-virtualized-auto-sizer": "^1.0.5",
"react-waypoint": "^10.1.0",
"react-window": "^1.8.6",
"reakit": "^1.3.6",
"reakit": "^1.3.8",
"regenerator-runtime": "^0.13.7",
"rich-markdown-editor": "^11.10.0",
"rich-markdown-editor": "^11.13.0",
"semver": "^7.3.2",
"sequelize": "^6.3.4",
"sequelize-cli": "^6.2.0",
@@ -178,7 +153,7 @@
"slate-md-serializer": "5.5.4",
"slug": "^4.0.4",
"smooth-scroll-into-view-if-needed": "^1.1.29",
"socket.io": "^2.3.0",
"socket.io": "^2.4.0",
"socket.io-redis": "^5.4.0",
"socketio-auth": "^0.1.1",
"string-replace-to-array": "^1.0.3",
@@ -200,6 +175,8 @@
"babel-jest": "^26.2.2",
"babel-loader": "^8.1.0",
"babel-plugin-transform-inline-environment-variables": "^0.4.3",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.6",
"eslint": "^7.6.0",
"eslint-config-react-app": "3.0.6",
"eslint-plugin-flowtype": "^5.2.0",
@@ -225,11 +202,13 @@
"webpack-cli": "^3.3.12",
"webpack-manifest-plugin": "^3.0.0",
"webpack-pwa-manifest": "^4.3.0",
"workbox-webpack-plugin": "^6.1.0"
"workbox-webpack-plugin": "^6.1.0",
"yarn-deduplicate": "^3.1.0"
},
"resolutions": {
"prosemirror-view": "1.18.1",
"dot-prop": "^5.2.0",
"js-yaml": "^3.13.1"
},
"version": "0.56.0"
"version": "0.57.0"
}
+1 -1
View File
@@ -7,7 +7,7 @@
],
"setupFiles": [
"<rootDir>/__mocks__/console.js",
"./server/test/helper.js"
"./server/test/setup.js"
],
"testEnvironment": "node"
}
+43 -13
View File
@@ -5,6 +5,7 @@ import { subtractDate } from "../../shared/utils/date";
import documentCreator from "../commands/documentCreator";
import documentImporter from "../commands/documentImporter";
import documentMover from "../commands/documentMover";
import { documentPermanentDeleter } from "../commands/documentPermanentDeleter";
import env from "../env";
import {
NotFoundError,
@@ -1174,24 +1175,53 @@ router.post("documents.archive", auth(), async (ctx) => {
});
router.post("documents.delete", auth(), async (ctx) => {
const { id } = ctx.body;
const { id, permanent } = ctx.body;
ctx.assertPresent(id, "id is required");
const user = ctx.state.user;
const document = await Document.findByPk(id, { userId: user.id });
authorize(user, "delete", document);
if (permanent) {
const document = await Document.findByPk(id, {
userId: user.id,
paranoid: false,
});
authorize(user, "permanentDelete", document);
await document.delete(user.id);
await Document.update(
{ parentDocumentId: null },
{
where: {
parentDocumentId: document.id,
},
paranoid: false,
}
);
await Event.create({
name: "documents.delete",
documentId: document.id,
collectionId: document.collectionId,
teamId: document.teamId,
actorId: user.id,
data: { title: document.title },
ip: ctx.request.ip,
});
await documentPermanentDeleter([document]);
await Event.create({
name: "documents.permanent_delete",
documentId: document.id,
collectionId: document.collectionId,
teamId: document.teamId,
actorId: user.id,
data: { title: document.title },
ip: ctx.request.ip,
});
} else {
const document = await Document.findByPk(id, { userId: user.id });
authorize(user, "delete", document);
await document.delete(user.id);
await Event.create({
name: "documents.delete",
documentId: document.id,
collectionId: document.collectionId,
teamId: document.teamId,
actorId: user.id,
data: { title: document.title },
ip: ctx.request.ip,
});
}
ctx.body = {
success: true,
+35
View File
@@ -984,6 +984,21 @@ describe("#documents.search", () => {
expect(body.data.length).toEqual(0);
});
it("should not error when search term is very long", async () => {
const { user } = await seed();
const res = await server.post("/api/documents.search", {
body: {
token: user.getJwtToken(),
query:
"much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much much longer search term",
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.length).toEqual(0);
});
it("should return draft documents created by user if chosen", async () => {
const { user } = await seed();
const document = await buildDocument({
@@ -2186,6 +2201,26 @@ describe("#documents.delete", () => {
expect(body.success).toEqual(true);
});
it("should allow permanently deleting a document", async () => {
const user = await buildUser();
const document = await buildDocument({
userId: user.id,
teamId: user.teamId,
});
await server.post("/api/documents.delete", {
body: { token: user.getJwtToken(), id: document.id },
});
const res = await server.post("/api/documents.delete", {
body: { token: user.getJwtToken(), id: document.id, permanent: true },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.success).toEqual(true);
});
it("should allow deleting document without collection", async () => {
const { user, document, collection } = await seed();
+7 -4
View File
@@ -16,6 +16,7 @@ router.post("events.list", auth(), pagination(), async (ctx) => {
let {
sort = "createdAt",
actorId,
documentId,
collectionId,
direction,
name,
@@ -31,10 +32,12 @@ router.post("events.list", auth(), pagination(), async (ctx) => {
if (actorId) {
ctx.assertUuid(actorId, "actorId must be a UUID");
where = {
...where,
actorId,
};
where = { ...where, actorId };
}
if (documentId) {
ctx.assertUuid(documentId, "documentId must be a UUID");
where = { ...where, documentId };
}
if (collectionId) {
+49 -1
View File
@@ -1,7 +1,7 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import TestServer from "fetch-test-server";
import app from "../app";
import { buildEvent } from "../test/factories";
import { buildEvent, buildUser } from "../test/factories";
import { flushdb, seed } from "../test/support";
const server = new TestServer(app.callback());
@@ -101,6 +101,54 @@ describe("#events.list", () => {
expect(body.data[0].id).toEqual(auditEvent.id);
});
it("should allow filtering by documentId", async () => {
const { user, admin, document, collection } = await seed();
const event = await buildEvent({
name: "documents.publish",
collectionId: collection.id,
documentId: document.id,
teamId: user.teamId,
actorId: user.id,
});
const res = await server.post("/api/events.list", {
body: {
token: admin.getJwtToken(),
documentId: document.id,
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.length).toEqual(1);
expect(body.data[0].id).toEqual(event.id);
});
it("should not return events for documentId without authorization", async () => {
const { user, document, collection } = await seed();
const actor = await buildUser();
await buildEvent({
name: "documents.publish",
collectionId: collection.id,
documentId: document.id,
teamId: user.teamId,
actorId: user.id,
});
const res = await server.post("/api/events.list", {
body: {
token: actor.getJwtToken(),
documentId: document.id,
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.length).toEqual(0);
});
it("should allow filtering by event name", async () => {
const { user, admin, document, collection } = await seed();
+6 -51
View File
@@ -2,10 +2,10 @@
import { subDays } from "date-fns";
import debug from "debug";
import Router from "koa-router";
import { documentPermanentDeleter } from "../commands/documentPermanentDeleter";
import { AuthenticationError } from "../errors";
import { Document, Attachment } from "../models";
import { Op, sequelize } from "../sequelize";
import parseAttachmentIds from "../utils/parseAttachmentIds";
import { Document } from "../models";
import { Op } from "../sequelize";
const router = new Router();
const log = debug("utils");
@@ -20,7 +20,7 @@ router.post("utils.gc", async (ctx) => {
log(`Permanently destroying upto ${limit} documents older than 30 days…`);
const documents = await Document.scope("withUnpublished").findAll({
attributes: ["id", "teamId", "text"],
attributes: ["id", "teamId", "text", "deletedAt"],
where: {
deletedAt: {
[Op.lt]: subDays(new Date(), 30),
@@ -30,54 +30,9 @@ router.post("utils.gc", async (ctx) => {
limit,
});
const query = `
SELECT COUNT(id)
FROM documents
WHERE "searchVector" @@ to_tsquery('english', :query) AND
"teamId" = :teamId AND
"id" != :documentId
`;
const countDeletedDocument = await documentPermanentDeleter(documents);
for (const document of documents) {
const attachmentIds = parseAttachmentIds(document.text);
for (const attachmentId of attachmentIds) {
const [{ count }] = await sequelize.query(query, {
type: sequelize.QueryTypes.SELECT,
replacements: {
documentId: document.id,
teamId: document.teamId,
query: attachmentId,
},
});
if (parseInt(count) === 0) {
const attachment = await Attachment.findOne({
where: {
teamId: document.teamId,
id: attachmentId,
},
});
if (attachment) {
await attachment.destroy();
log(`Attachment ${attachmentId} deleted`);
} else {
log(`Unknown attachment ${attachmentId} ignored`);
}
}
}
}
await Document.scope("withUnpublished").destroy({
where: {
id: documents.map((document) => document.id),
},
force: true,
});
log(`Destroyed ${documents.length} documents`);
log(`Destroyed ${countDeletedDocument} documents`);
ctx.body = {
success: true,
+2 -90
View File
@@ -2,8 +2,8 @@
import { subDays } from "date-fns";
import TestServer from "fetch-test-server";
import app from "../app";
import { Attachment, Document } from "../models";
import { buildAttachment, buildDocument } from "../test/factories";
import { Document } from "../models";
import { buildDocument } from "../test/factories";
import { flushdb } from "../test/support";
const server = new TestServer(app.callback());
@@ -67,94 +67,6 @@ describe("#utils.gc", () => {
expect(await Document.unscoped().count({ paranoid: false })).toEqual(0);
});
it("should destroy attachments no longer referenced", async () => {
const document = await buildDocument({
publishedAt: subDays(new Date(), 90),
deletedAt: subDays(new Date(), 60),
});
const attachment = await buildAttachment({
teamId: document.teamId,
documentId: document.id,
});
document.text = `![text](${attachment.redirectUrl})`;
await document.save();
const res = await server.post("/api/utils.gc", {
body: {
token: process.env.UTILS_SECRET,
},
});
expect(res.status).toEqual(200);
expect(await Attachment.count()).toEqual(0);
expect(await Document.unscoped().count({ paranoid: false })).toEqual(0);
});
it("should handle unknown attachment ids", async () => {
const document = await buildDocument({
publishedAt: subDays(new Date(), 90),
deletedAt: subDays(new Date(), 60),
});
const attachment = await buildAttachment({
teamId: document.teamId,
documentId: document.id,
});
document.text = `![text](${attachment.redirectUrl})`;
await document.save();
// remove attachment so it no longer exists in the database, this is also
// representative of a corrupt attachment id in the doc or the regex returning
// an incorrect string
await attachment.destroy({ force: true });
const res = await server.post("/api/utils.gc", {
body: {
token: process.env.UTILS_SECRET,
},
});
expect(res.status).toEqual(200);
expect(await Attachment.count()).toEqual(0);
expect(await Document.unscoped().count({ paranoid: false })).toEqual(0);
});
it("should not destroy attachments referenced in other documents", async () => {
const document1 = await buildDocument();
const document = await buildDocument({
teamId: document1.teamId,
publishedAt: subDays(new Date(), 90),
deletedAt: subDays(new Date(), 60),
});
const attachment = await buildAttachment({
teamId: document1.teamId,
documentId: document.id,
});
document1.text = `![text](${attachment.redirectUrl})`;
await document1.save();
document.text = `![text](${attachment.redirectUrl})`;
await document.save();
expect(await Attachment.count()).toEqual(1);
const res = await server.post("/api/utils.gc", {
body: {
token: process.env.UTILS_SECRET,
},
});
expect(res.status).toEqual(200);
expect(await Attachment.count()).toEqual(1);
expect(await Document.unscoped().count({ paranoid: false })).toEqual(1);
});
it("should destroy draft documents deleted more than 30 days ago", async () => {
await buildDocument({
publishedAt: undefined,
+46 -6
View File
@@ -2,12 +2,15 @@
import { subMinutes } from "date-fns";
import Router from "koa-router";
import { find } from "lodash";
import { parseDomain, isCustomSubdomain } from "../../../shared/utils/domains";
import { AuthorizationError } from "../../errors";
import mailer, { sendEmail } from "../../mailer";
import errorHandling from "../../middlewares/errorHandling";
import methodOverride from "../../middlewares/methodOverride";
import validation from "../../middlewares/validation";
import { User, Team } from "../../models";
import { signIn } from "../../utils/authentication";
import { isCustomDomain } from "../../utils/domains";
import { getUserForEmailSigninToken } from "../../utils/jwt";
const router = new Router();
@@ -20,19 +23,56 @@ export const config = {
router.use(methodOverride());
router.use(validation());
router.post("email", async (ctx) => {
router.post("email", errorHandling(), async (ctx) => {
const { email } = ctx.body;
ctx.assertEmail(email, "email is required");
const user = await User.scope("withAuthentications").findOne({
const users = await User.scope("withAuthentications").findAll({
where: { email: email.toLowerCase() },
});
if (user) {
const team = await Team.scope("withAuthenticationProviders").findByPk(
user.teamId
);
if (users.length) {
let team;
if (isCustomDomain(ctx.request.hostname)) {
team = await Team.scope("withAuthenticationProviders").findOne({
where: { domain: ctx.request.hostname },
});
}
if (
process.env.SUBDOMAINS_ENABLED === "true" &&
isCustomSubdomain(ctx.request.hostname) &&
!isCustomDomain(ctx.request.hostname)
) {
const domain = parseDomain(ctx.request.hostname);
const subdomain = domain ? domain.subdomain : undefined;
team = await Team.scope("withAuthenticationProviders").findOne({
where: { subdomain },
});
}
// If there are multiple users with this email address then give precedence
// to the one that is active on this subdomain/domain (if any)
let user = users.find((user) => team && user.teamId === team.id);
// A user was found for the email address, but they don't belong to the team
// that this subdomain belongs to, we load their team and allow the logic to
// continue
if (!user) {
user = users[0];
team = await Team.scope("withAuthenticationProviders").findByPk(
user.teamId
);
}
if (!team) {
team = await Team.scope("withAuthenticationProviders").findByPk(
user.teamId
);
}
if (!team) {
ctx.redirect(`/?notice=auth-error`);
return;
+177
View File
@@ -0,0 +1,177 @@
// @flow
import TestServer from "fetch-test-server";
import app from "../../app";
import mailer from "../../mailer";
import { buildUser, buildGuestUser, buildTeam } from "../../test/factories";
import { flushdb } from "../../test/support";
const server = new TestServer(app.callback());
jest.mock("../../mailer");
beforeEach(async () => {
await flushdb();
// $FlowFixMe does not understand Jest mocks
mailer.signin.mockReset();
});
afterAll(() => server.close());
describe("email", () => {
it("should require email param", async () => {
const res = await server.post("/auth/email", {
body: {},
});
const body = await res.json();
expect(res.status).toEqual(400);
expect(body.error).toEqual("validation_error");
expect(body.ok).toEqual(false);
});
it("should respond with redirect location when user is SSO enabled", async () => {
const user = await buildUser();
const res = await server.post("/auth/email", {
body: { email: user.email },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.redirect).toMatch("slack");
expect(mailer.signin).not.toHaveBeenCalled();
});
it("should respond with redirect location when user is SSO enabled on another subdomain", async () => {
process.env.URL = "http://localoutline.com";
process.env.SUBDOMAINS_ENABLED = "true";
const user = await buildUser();
await buildTeam({
subdomain: "example",
});
const res = await server.post("/auth/email", {
body: { email: user.email },
headers: { host: "example.localoutline.com" },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.redirect).toMatch("slack");
expect(mailer.signin).not.toHaveBeenCalled();
});
it("should respond with success when user is not SSO enabled", async () => {
const user = await buildGuestUser();
const res = await server.post("/auth/email", {
body: { email: user.email },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.success).toEqual(true);
expect(mailer.signin).toHaveBeenCalled();
});
it("should respond with success regardless of whether successful to prevent crawling email logins", async () => {
const res = await server.post("/auth/email", {
body: { email: "user@example.com" },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.success).toEqual(true);
expect(mailer.signin).not.toHaveBeenCalled();
});
describe("with multiple users matching email", () => {
it("should default to current subdomain with SSO", async () => {
process.env.URL = "http://localoutline.com";
process.env.SUBDOMAINS_ENABLED = "true";
const email = "sso-user@example.org";
const team = await buildTeam({
subdomain: "example",
});
await buildGuestUser({ email });
await buildUser({ email, teamId: team.id });
const res = await server.post("/auth/email", {
body: { email },
headers: { host: "example.localoutline.com" },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.redirect).toMatch("slack");
expect(mailer.signin).not.toHaveBeenCalled();
});
it("should default to current subdomain with guest email", async () => {
process.env.URL = "http://localoutline.com";
process.env.SUBDOMAINS_ENABLED = "true";
const email = "guest-user@example.org";
const team = await buildTeam({
subdomain: "example",
});
await buildUser({ email });
await buildGuestUser({ email, teamId: team.id });
const res = await server.post("/auth/email", {
body: { email },
headers: { host: "example.localoutline.com" },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.success).toEqual(true);
expect(mailer.signin).toHaveBeenCalled();
});
it("should default to custom domain with SSO", async () => {
const email = "sso-user-2@example.org";
const team = await buildTeam({
domain: "docs.mycompany.com",
});
await buildGuestUser({ email });
await buildUser({ email, teamId: team.id });
const res = await server.post("/auth/email", {
body: { email },
headers: { host: "docs.mycompany.com" },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.redirect).toMatch("slack");
expect(mailer.signin).not.toHaveBeenCalled();
});
it("should default to custom domain with guest email", async () => {
const email = "guest-user-2@example.org";
const team = await buildTeam({
domain: "docs.mycompany.com",
});
await buildUser({ email });
await buildGuestUser({ email, teamId: team.id });
const res = await server.post("/auth/email", {
body: { email },
headers: { host: "docs.mycompany.com" },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.success).toEqual(true);
expect(mailer.signin).toHaveBeenCalled();
});
});
});
@@ -0,0 +1,64 @@
// @flow
import debug from "debug";
import { Document, Attachment } from "../models";
import { sequelize } from "../sequelize";
import parseAttachmentIds from "../utils/parseAttachmentIds";
const log = debug("commands");
export async function documentPermanentDeleter(documents: Document[]) {
const activeDocument = documents.find((doc) => !doc.deletedAt);
if (activeDocument) {
throw new Error(
`Cannot permanently delete ${activeDocument.id} document. Please delete it and try again.`
);
}
const query = `
SELECT COUNT(id)
FROM documents
WHERE "searchVector" @@ to_tsquery('english', :query) AND
"teamId" = :teamId AND
"id" != :documentId
`;
for (const document of documents) {
const attachmentIds = parseAttachmentIds(document.text);
for (const attachmentId of attachmentIds) {
const [{ count }] = await sequelize.query(query, {
type: sequelize.QueryTypes.SELECT,
replacements: {
documentId: document.id,
teamId: document.teamId,
query: attachmentId,
},
});
if (parseInt(count) === 0) {
const attachment = await Attachment.findOne({
where: {
teamId: document.teamId,
id: attachmentId,
},
});
if (attachment) {
await attachment.destroy();
log(`Attachment ${attachmentId} deleted`);
} else {
log(`Unknown attachment ${attachmentId} ignored`);
}
}
}
}
return Document.scope("withUnpublished").destroy({
where: {
id: documents.map((document) => document.id),
},
force: true,
});
}
@@ -0,0 +1,123 @@
// @flow
import { subDays } from "date-fns";
import { Attachment, Document } from "../models";
import { buildAttachment, buildDocument } from "../test/factories";
import { flushdb } from "../test/support";
import { documentPermanentDeleter } from "./documentPermanentDeleter";
jest.mock("aws-sdk", () => {
const mS3 = { deleteObject: jest.fn().mockReturnThis(), promise: jest.fn() };
return {
S3: jest.fn(() => mS3),
Endpoint: jest.fn(),
};
});
beforeEach(() => flushdb());
describe("documentPermanentDeleter", () => {
it("should destroy documents", async () => {
const document = await buildDocument({
publishedAt: subDays(new Date(), 90),
deletedAt: new Date(),
});
const countDeletedDoc = await documentPermanentDeleter([document]);
expect(countDeletedDoc).toEqual(1);
expect(await Document.unscoped().count({ paranoid: false })).toEqual(0);
});
it("should error when trying to destroy undeleted documents", async () => {
const document = await buildDocument({
publishedAt: new Date(),
});
let error;
try {
await documentPermanentDeleter([document]);
} catch (err) {
error = err.message;
}
expect(error).toEqual(
`Cannot permanently delete ${document.id} document. Please delete it and try again.`
);
});
it("should destroy attachments no longer referenced", async () => {
const document = await buildDocument({
publishedAt: subDays(new Date(), 90),
deletedAt: new Date(),
});
const attachment = await buildAttachment({
teamId: document.teamId,
documentId: document.id,
});
document.text = `![text](${attachment.redirectUrl})`;
await document.save();
const countDeletedDoc = await documentPermanentDeleter([document]);
expect(countDeletedDoc).toEqual(1);
expect(await Attachment.count()).toEqual(0);
expect(await Document.unscoped().count({ paranoid: false })).toEqual(0);
});
it("should handle unknown attachment ids", async () => {
const document = await buildDocument({
publishedAt: subDays(new Date(), 90),
deletedAt: new Date(),
});
const attachment = await buildAttachment({
teamId: document.teamId,
documentId: document.id,
});
document.text = `![text](${attachment.redirectUrl})`;
await document.save();
// remove attachment so it no longer exists in the database, this is also
// representative of a corrupt attachment id in the doc or the regex returning
// an incorrect string
await attachment.destroy({ force: true });
const countDeletedDoc = await documentPermanentDeleter([document]);
expect(countDeletedDoc).toEqual(1);
expect(await Attachment.count()).toEqual(0);
expect(await Document.unscoped().count({ paranoid: false })).toEqual(0);
});
it("should not destroy attachments referenced in other documents", async () => {
const document1 = await buildDocument();
const document = await buildDocument({
teamId: document1.teamId,
publishedAt: subDays(new Date(), 90),
deletedAt: subDays(new Date(), 60),
});
const attachment = await buildAttachment({
teamId: document1.teamId,
documentId: document.id,
});
document1.text = `![text](${attachment.redirectUrl})`;
await document1.save();
document.text = `![text](${attachment.redirectUrl})`;
await document.save();
expect(await Attachment.count()).toEqual(1);
const countDeletedDoc = await documentPermanentDeleter([document]);
expect(countDeletedDoc).toEqual(1);
expect(await Attachment.count()).toEqual(1);
expect(await Document.unscoped().count({ paranoid: false })).toEqual(1);
});
});
+4 -2
View File
@@ -41,11 +41,13 @@ export const CollectionNotificationEmail = ({
<Body>
<Heading>{collection.name}</Heading>
<p>
{actor.name} {eventName} the collection "{collection.name}".
{actor.name} {eventName} the collection {collection.name}.
</p>
<EmptySpace height={10} />
<p>
<Button href={`${process.env.URL}${collection.url}`}>
<Button
href={`${process.env.URL}${collection.url}?ref=notification-email`}
>
Open Collection
</Button>
</p>
+225 -6
View File
@@ -1,8 +1,10 @@
// @flow
import * as React from "react";
import theme from "../../shared/styles/theme";
import { User, Document, Team, Collection } from "../models";
import Body from "./components/Body";
import Button from "./components/Button";
import Diff from "./components/Diff";
import EmailTemplate from "./components/EmailLayout";
import EmptySpace from "./components/EmptySpace";
import Footer from "./components/Footer";
@@ -15,6 +17,7 @@ export type Props = {
document: Document,
collection: Collection,
eventName: string,
summary: string,
unsubscribeUrl: string,
};
@@ -38,26 +41,34 @@ export const DocumentNotificationEmail = ({
document,
collection,
eventName = "published",
summary,
unsubscribeUrl,
}: Props) => {
const link = `${team.url}${document.url}?ref=notification-email`;
return (
<EmailTemplate>
<Header />
<Body>
<Heading>
"{document.title}" {eventName}
{document.title} {eventName}
</Heading>
<p>
{actor.name} {eventName} the document "{document.title}", in the{" "}
{collection.name} collection.
</p>
<hr />
<EmptySpace height={10} />
<p>{document.getSummary()}</p>
<EmptySpace height={10} />
{summary && (
<>
<EmptySpace height={20} />
<Diff href={link}>
<div dangerouslySetInnerHTML={{ __html: summary }} />
</Diff>
<EmptySpace height={20} />
</>
)}
<p>
<Button href={`${team.url}${document.url}`}>Open Document</Button>
<Button href={link}>Open Document</Button>
</p>
</Body>
@@ -65,3 +76,211 @@ export const DocumentNotificationEmail = ({
</EmailTemplate>
);
};
export const css = `
font-family: ${theme.fontFamily};
font-weight: ${theme.fontWeight};
font-size: 1em;
line-height: 1.7em;
pre {
white-space: pre-wrap;
}
img {
text-align: center;
max-width: 100%;
max-height: 75vh;
clear: both;
}
img.image-right-50 {
float: right;
width: 50%;
margin-left: 2em;
margin-bottom: 1em;
clear: initial;
}
img.image-left-50 {
float: left;
width: 50%;
margin-right: 2em;
margin-bottom: 1em;
clear: initial;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 1em 0 0.5em;
font-weight: 500;
}
.notice {
display: flex;
align-items: center;
background: ${theme.noticeInfoBackground};
color: ${theme.noticeInfoText};
border-radius: 4px;
padding: 8px 16px;
margin: 8px 0;
}
.notice-tip {
background: ${theme.noticeTipBackground};
color: ${theme.noticeTipText};
}
.notice-warning {
background: ${theme.noticeWarningBackground};
color: ${theme.noticeWarningText};
}
b,
strong {
font-weight: 600;
}
p {
margin: 0;
}
a {
color: ${theme.link};
}
ins {
background-color: #128a2929;
text-decoration: none;
}
del {
background-color: ${theme.slateLight};
color: ${theme.slate};
text-decoration: strikethrough;
}
hr {
position: relative;
height: 1em;
border: 0;
}
hr:before {
content: "";
display: block;
position: absolute;
border-top: 1px solid ${theme.horizontalRule};
top: 0.5em;
left: 0;
right: 0;
}
hr.page-break {
page-break-after: always;
}
hr.page-break:before {
border-top: 1px dashed ${theme.horizontalRule};
}
code {
border-radius: 4px;
border: 1px solid ${theme.codeBorder};
padding: 3px 4px;
font-family: ${theme.fontFamilyMono};
font-size: 85%;
}
mark {
border-radius: 1px;
color: ${theme.textHighlightForeground};
background: ${theme.textHighlight};
a {
color: ${theme.textHighlightForeground};
}
}
ul {
padding-left: 0;
}
.checkbox-list-item {
list-style: none;
padding: 4px 0;
margin: 0;
}
.checkbox {
font-size: 0;
display: block;
float: left;
white-space: nowrap;
width: 12px;
height: 12px;
margin-top: 2px;
margin-right: 8px;
border: 1px solid ${theme.textSecondary};
border-radius: 3px;
}
pre {
display: block;
overflow-x: auto;
padding: 0.75em 1em;
line-height: 1.4em;
position: relative;
background: ${theme.codeBackground};
border-radius: 4px;
border: 1px solid ${theme.codeBorder};
-webkit-font-smoothing: initial;
font-family: ${theme.fontFamilyMono};
font-size: 13px;
direction: ltr;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
margin: 0;
code {
font-size: 13px;
background: none;
padding: 0;
border: 0;
}
}
table {
width: 100%;
border-collapse: collapse;
border-radius: 4px;
margin-top: 1em;
box-sizing: border-box;
* {
box-sizing: border-box;
}
tr {
position: relative;
border-bottom: 1px solid ${theme.tableDivider};
}
td,
th {
position: relative;
vertical-align: top;
border: 1px solid ${theme.tableDivider};
position: relative;
padding: 4px 8px;
min-width: 100px;
}
}
`;
+1 -1
View File
@@ -47,7 +47,7 @@ export const InviteEmail = ({
</p>
<EmptySpace height={10} />
<p>
<Button href={teamUrl}>Join now</Button>
<Button href={`${teamUrl}?ref=invite-email`}>Join now</Button>
</p>
</Body>
+3 -1
View File
@@ -43,7 +43,9 @@ export const WelcomeEmail = ({ teamUrl }: Props) => {
</p>
<EmptySpace height={10} />
<p>
<Button href={`${teamUrl}/home`}>View my dashboard</Button>
<Button href={`${teamUrl}/home?ref=welcome-email`}>
View my dashboard
</Button>
</p>
</Body>
+25
View File
@@ -0,0 +1,25 @@
// @flow
import * as React from "react";
import theme from "../../../shared/styles/theme";
type Props = {|
children: React.Node,
href?: string,
|};
export default ({ children, ...rest }: Props) => {
const style = {
borderRadius: "4px",
background: theme.secondaryBackground,
padding: ".5em 1em",
color: theme.text,
display: "block",
textDecoration: "none",
};
return (
<a width="100%" style={style} {...rest}>
{children}
</a>
);
};
+2 -2
View File
@@ -3,9 +3,9 @@ import { Table, TBody, TR, TD } from "oy-vey";
import * as React from "react";
import theme from "../../../shared/styles/theme";
type Props = {
type Props = {|
children: React.Node,
};
|};
export default (props: Props) => (
<Table width="550" padding="40">
+3
View File
@@ -35,6 +35,7 @@ export type DocumentEvent =
name: | "documents.create" // eslint-disable-line
| "documents.publish"
| "documents.delete"
| "documents.permanent_delete"
| "documents.pin"
| "documents.unpin"
| "documents.archive"
@@ -99,6 +100,8 @@ export type RevisionEvent = {
documentId: string,
collectionId: string,
teamId: string,
actorId: string,
modelId: string,
};
export type CollectionImportEvent = {
+19 -12
View File
@@ -13,6 +13,7 @@ import {
type Props as DocumentNotificationEmailT,
DocumentNotificationEmail,
documentNotificationEmailText,
css as documentNotificationEmailCSS,
} from "./emails/DocumentNotificationEmail";
import { ExportEmail, exportEmailText } from "./emails/ExportEmail";
import {
@@ -146,8 +147,9 @@ export class Mailer {
this.sendMail({
to: opts.to,
title: `${opts.document.title}${opts.eventName}`,
previewText: `${opts.actor.name} ${opts.eventName} a new document`,
previewText: `${opts.actor.name} ${opts.eventName} a document`,
html: <DocumentNotificationEmail {...opts} />,
headCSS: documentNotificationEmailCSS,
text: documentNotificationEmailText(opts),
});
};
@@ -197,19 +199,24 @@ export class Mailer {
if (useTestEmailService) {
log("SMTP_USERNAME not provided, generating test account…");
let testAccount = await nodemailer.createTestAccount();
const smtpConfig = {
host: "smtp.ethereal.email",
port: 587,
secure: false,
auth: {
user: testAccount.user,
pass: testAccount.pass,
},
};
try {
let testAccount = await nodemailer.createTestAccount();
this.transporter = nodemailer.createTransport(smtpConfig);
const smtpConfig = {
host: "smtp.ethereal.email",
port: 587,
secure: false,
auth: {
user: testAccount.user,
pass: testAccount.pass,
},
};
this.transporter = nodemailer.createTransport(smtpConfig);
} catch (err) {
log(`Could not generate test account: ${err.message}`);
}
}
}
}
+4
View File
@@ -30,6 +30,10 @@ io.adapter(
})
);
io.origins((_, callback) => {
callback(null, true);
});
io.of("/").adapter.on("error", (err) => {
if (err.name === "MaxRetriesPerRequestError") {
console.error(`Redis error: ${err.message}. Shutting down now.`);
+9 -1
View File
@@ -9,7 +9,7 @@ export default function createMiddleware(providerName: string) {
return passport.authorize(
providerName,
{ session: false },
async (err, _, result: AccountProvisionerResult) => {
async (err, user, result: AccountProvisionerResult) => {
if (err) {
console.error(err);
@@ -24,6 +24,14 @@ export default function createMiddleware(providerName: string) {
return ctx.redirect(`/?notice=auth-error`);
}
// Passport.js may invoke this callback with err=null and user=null in
// the event that error=access_denied is received from the OAuth server.
// I'm not sure why this exception to the rule exists, but it does:
// https://github.com/jaredhanson/passport-oauth2/blob/e20f26aad60ed54f0e7952928cbb64979ef8da2b/lib/strategy.js#L135
if (!user) {
return ctx.redirect(`/?notice=auth-error`);
}
// Handle errors from Azure which come in the format: message, Trace ID,
// Correlation ID, Timestamp in these two query string parameters.
const { error, error_description } = ctx.request.query;
@@ -1,15 +1,7 @@
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.changeColumn('users', 'email', {
type: Sequelize.STRING,
unique: false,
allowNull: false,
});
await queryInterface.changeColumn('users', 'username', {
type: Sequelize.STRING,
unique: false,
allowNull: false,
});
await queryInterface.removeConstraint('users', 'users_email_key', {})
await queryInterface.removeConstraint('users', 'users_username_key', {})
},
down: async (queryInterface, Sequelize) => {
+5 -2
View File
@@ -395,7 +395,11 @@ Collection.prototype.isChildDocument = function (
let result = false;
const loopChildren = (documents, input) => {
return documents.map((document) => {
if (result) {
return;
}
documents.forEach((document) => {
let parents = [...input];
if (document.id === documentId) {
result = parents.includes(parentDocumentId);
@@ -403,7 +407,6 @@ Collection.prototype.isChildDocument = function (
parents.push(document.id);
loopChildren(document.children, parents);
}
return document;
});
};
+2
View File
@@ -65,6 +65,7 @@ Event.ACTIVITY_EVENTS = [
"documents.unpin",
"documents.move",
"documents.delete",
"documents.permanent_delete",
"documents.restore",
"users.create",
];
@@ -90,6 +91,7 @@ Event.AUDIT_EVENTS = [
"documents.unpin",
"documents.move",
"documents.delete",
"documents.permanent_delete",
"documents.restore",
"groups.create",
"groups.update",
+3
View File
@@ -15,6 +15,9 @@ const SearchQuery = sequelize.define(
},
query: {
type: DataTypes.STRING,
set(val) {
this.setDataValue("query", val.substring(0, 255));
},
allowNull: false,
},
results: {
+15 -3
View File
@@ -101,8 +101,15 @@ allow(User, ["pin", "unpin"], Document, (user, document) => {
});
allow(User, "delete", Document, (user, document) => {
// unpublished drafts can always be deleted
if (user.isViewer) return false;
if (document.deletedAt) return false;
// allow deleting document without a collection
if (document.collection && cannot(user, "update", document.collection)) {
return false;
}
// unpublished drafts can always be deleted
if (
!document.deletedAt &&
!document.publishedAt &&
@@ -111,13 +118,18 @@ allow(User, "delete", Document, (user, document) => {
return true;
}
return user.teamId === document.teamId;
});
allow(User, "permanentDelete", Document, (user, document) => {
if (user.isViewer) return false;
if (!document.deletedAt) return false;
// allow deleting document without a collection
if (document.collection && cannot(user, "update", document.collection)) {
return false;
}
if (document.deletedAt) return false;
return user.teamId === document.teamId;
});
+143 -22
View File
@@ -1,32 +1,60 @@
// @flow
import debug from "debug";
import type { DocumentEvent, CollectionEvent, Event } from "../events";
import type {
DocumentEvent,
RevisionEvent,
CollectionEvent,
Event,
} from "../events";
import mailer from "../mailer";
import {
View,
Document,
Team,
Collection,
Revision,
User,
NotificationSetting,
Attachment,
} from "../models";
import { Op } from "../sequelize";
import markdownDiff from "../utils/markdownDiff";
import parseAttachmentIds from "../utils/parseAttachmentIds";
import { getSignedImageUrl } from "../utils/s3";
const log = debug("services");
async function replaceImageAttachments(text: string) {
const attachmentIds = parseAttachmentIds(text);
await Promise.all(
attachmentIds.map(async (id) => {
const attachment = await Attachment.findByPk(id);
if (attachment) {
const accessUrl = await getSignedImageUrl(attachment.key, 86400 * 4);
text = text.replace(attachment.redirectUrl, accessUrl);
}
})
);
return text;
}
export default class Notifications {
async on(event: Event) {
switch (event.name) {
case "documents.publish":
case "documents.update.debounced":
return this.documentUpdated(event);
return this.documentPublished(event);
case "revisions.create":
return this.revisionCreated(event);
case "collections.create":
return this.collectionCreated(event);
default:
}
}
async documentUpdated(event: DocumentEvent) {
async documentPublished(event: DocumentEvent) {
// never send notifications when batch importing documents
if (event.data && event.data.source === "import") return;
@@ -45,10 +73,7 @@ export default class Notifications {
[Op.ne]: document.lastModifiedById,
},
teamId: document.teamId,
event:
event.name === "documents.publish"
? "documents.publish"
: "documents.update",
event: "documents.publish",
},
include: [
{
@@ -59,25 +84,14 @@ export default class Notifications {
],
});
const eventName =
event.name === "documents.publish" ? "published" : "updated";
const eventName = "published";
for (const setting of notificationSettings) {
// For document updates we only want to send notifications if
// the document has been edited by the user with this notification setting
// This could be replaced with ability to "follow" in the future
if (
eventName === "updated" &&
!document.collaboratorIds.includes(setting.userId)
) {
return;
}
// Check the user has access to the collection this document is in. Just
// because they were a collaborator once doesn't mean they still are.
const collectionIds = await setting.user.collectionIds();
if (!collectionIds.includes(document.collectionId)) {
return;
continue;
}
// If this user has viewed the document since the last update was made
@@ -96,7 +110,7 @@ export default class Notifications {
log(
`suppressing notification to ${setting.userId} because update viewed`
);
return;
continue;
}
mailer.documentNotification({
@@ -105,12 +119,119 @@ export default class Notifications {
document,
team,
collection,
summary: document.getSummary(),
actor: document.updatedBy,
unsubscribeUrl: setting.unsubscribeUrl,
});
}
}
async revisionCreated(event: RevisionEvent) {
const revision = await Revision.findByPk(event.modelId, {
include: [
{
model: Document,
as: "document",
include: [
{
model: Collection,
as: "collection",
},
],
},
],
});
if (!revision) return;
const { document } = revision;
const { collection } = document;
if (!collection || !document) return;
const team = await Team.findByPk(document.teamId);
if (!team) return;
const notificationSettings = await NotificationSetting.findAll({
where: {
userId: {
[Op.ne]: revision.userId,
},
teamId: document.teamId,
event: "documents.update",
},
include: [
{
model: User,
required: true,
as: "user",
},
],
});
const eventName = "updated";
for (const setting of notificationSettings) {
// For document updates we only want to send notifications if
// the document has been edited by the user with this notification setting
// This could be replaced with ability to "follow" in the future
if (!document.collaboratorIds.includes(setting.userId)) {
continue;
}
// Check the user has access to the collection this document is in. Just
// because they were a collaborator once doesn't mean they still are.
const collectionIds = await setting.user.collectionIds();
if (!collectionIds.includes(document.collectionId)) {
continue;
}
// If this user has viewed the document since the last update was made
// then we can avoid sending them a useless notification, yay.
const view = await View.findOne({
where: {
userId: setting.userId,
documentId: event.documentId,
updatedAt: {
[Op.gt]: document.updatedAt,
},
},
});
if (view) {
log(
`suppressing notification to ${setting.userId} because update viewed`
);
continue;
}
const previous = await Revision.findOne({
where: {
documentId: document.id,
createdAt: {
[Op.lt]: revision.createdAt,
},
},
order: [["createdAt", "DESC"]],
});
let summary = markdownDiff(previous ? previous.text : "", revision.text);
console.log(summary);
summary = await replaceImageAttachments(summary);
console.log(summary);
mailer.documentNotification({
to: setting.user.email,
eventName,
document,
team,
collection,
summary,
actor: revision.user,
unsubscribeUrl: setting.unsubscribeUrl,
});
}
}
async collectionCreated(event: CollectionEvent) {
const collection = await Collection.findByPk(event.collectionId, {
include: [
+12 -6
View File
@@ -1,6 +1,6 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import mailer from "../mailer";
import { View, NotificationSetting } from "../models";
import { View, NotificationSetting, Revision } from "../models";
import { buildDocument, buildCollection, buildUser } from "../test/factories";
import { flushdb } from "../test/support";
import NotificationsService from "./notifications";
@@ -89,9 +89,10 @@ describe("documents.publish", () => {
});
});
describe("documents.update.debounced", () => {
describe("revisions.create", () => {
test("should send a notification to other collaborator", async () => {
const document = await buildDocument();
const revision = await Revision.createFromDocument(document);
const collaborator = await buildUser({ teamId: document.teamId });
document.collaboratorIds = [collaborator.id];
await document.save();
@@ -103,8 +104,9 @@ describe("documents.update.debounced", () => {
});
await Notifications.on({
name: "documents.update.debounced",
name: "revisions.create",
documentId: document.id,
modelId: revision.id,
collectionId: document.collectionId,
teamId: document.teamId,
actorId: document.createdById,
@@ -115,6 +117,7 @@ describe("documents.update.debounced", () => {
test("should not send a notification if viewed since update", async () => {
const document = await buildDocument();
const revision = await Revision.createFromDocument(document);
const collaborator = await buildUser({ teamId: document.teamId });
document.collaboratorIds = [collaborator.id];
await document.save();
@@ -128,9 +131,10 @@ describe("documents.update.debounced", () => {
await View.touch(document.id, collaborator.id, true);
await Notifications.on({
name: "documents.update.debounced",
name: "revisions.create",
documentId: document.id,
collectionId: document.collectionId,
modelId: revision.id,
teamId: document.teamId,
actorId: document.createdById,
});
@@ -138,12 +142,13 @@ describe("documents.update.debounced", () => {
expect(mailer.documentNotification).not.toHaveBeenCalled();
});
test("should not send a notification to last editor", async () => {
test("should not send a notification to the last user that modified", async () => {
const user = await buildUser();
const document = await buildDocument({
teamId: user.teamId,
lastModifiedById: user.id,
});
const revision = await Revision.createFromDocument(document);
await NotificationSetting.create({
userId: user.id,
@@ -152,8 +157,9 @@ describe("documents.update.debounced", () => {
});
await Notifications.on({
name: "documents.update.debounced",
name: "revisions.create",
documentId: document.id,
modelId: revision.id,
collectionId: document.collectionId,
teamId: document.teamId,
actorId: document.createdById,
+10 -2
View File
@@ -1,6 +1,6 @@
// @flow
import type { DocumentEvent, RevisionEvent } from "../events";
import { Revision, Document } from "../models";
import { Revision, Document, Event } from "../models";
export default class Revisions {
async on(event: DocumentEvent | RevisionEvent) {
@@ -22,7 +22,15 @@ export default class Revisions {
return;
}
await Revision.createFromDocument(document);
const revision = await Revision.createFromDocument(document);
Event.add({
name: "revisions.create",
documentId: document.id,
collectionId: document.collectionId,
modelId: revision.id,
teamId: document.teamId,
actorId: revision.userId,
});
break;
}
+7
View File
@@ -79,6 +79,13 @@ export default class Websockets {
],
});
}
case "documents.permanent_delete": {
return socketio
.to(`collection-${event.collectionId}`)
.emit(event.name, {
documentId: event.documentId,
});
}
case "documents.pin":
case "documents.unpin":
case "documents.update": {
+17
View File
@@ -68,6 +68,23 @@ export function buildEvent(overrides: Object = {}) {
});
}
export async function buildGuestUser(overrides: Object = {}) {
if (!overrides.teamId) {
const team = await buildTeam();
overrides.teamId = team.id;
}
count++;
return User.create({
email: `user${count}@example.com`,
name: `User ${count}`,
createdAt: new Date("2018-01-01T00:00:00.000Z"),
lastActiveAt: new Date("2018-01-01T00:00:00.000Z"),
...overrides,
});
}
export async function buildUser(overrides: Object = {}) {
if (!overrides.teamId) {
const team = await buildTeam();
+34
View File
@@ -0,0 +1,34 @@
# Heading 1
## Heading 2
This is a test paragraph
This is a second test paragraph. This is a second sentence.
This is a another test paragraph. This is a another sentence.
- list item 1
- list item 2
```
this is a codeblock
```
:::info
This is an info block
:::
!!This is a placeholder!!
==this is a highlight==
- [ ] checklist item 1
- [ ] checklist item 2
- [x] checklist item 3
same on both sides
same on both sides
same on both sides
+37
View File
@@ -0,0 +1,37 @@
# Heading 1
## Heading 2
This is a test paragraph
This is a second test paragraph. This is a second sentence.
This is a another test paragraph. This is a another sentence.
- list item 1
```
this is a codeblock
```
This is a new paragraph.
:::info
This is an info block
:::
!!This is a placeholder!!
==this is a highlight==
- [x] checklist item 1
- [x] checklist item 2
- [ ] checklist item 3
- [ ] checklist item 4
- [x] checklist item 5
same on both sides
same on both sides
same on both sides
@@ -2,9 +2,11 @@
require("dotenv").config({ silent: true });
// test environment variables
process.env.URL = "http://localhost:3000";
process.env.DATABASE_URL = process.env.DATABASE_URL_TEST;
process.env.NODE_ENV = "test";
process.env.GOOGLE_CLIENT_ID = "123";
process.env.AZURE_CLIENT_ID = "";
process.env.SLACK_KEY = "123";
process.env.DEPLOYMENT = "";
process.env.ALLOWED_DOMAINS = "allowed-domain.com";
+8 -10
View File
@@ -3,17 +3,15 @@ import { v4 as uuidv4 } from "uuid";
import { User, Document, Collection, Team } from "../models";
import { sequelize } from "../sequelize";
export function flushdb() {
const sql = sequelize.getQueryInterface();
const tables = Object.keys(sequelize.models).map((model) => {
const n = sequelize.models[model].getTableName();
return sql.queryGenerator.quoteTable(
typeof n === "string" ? n : n.tableName
);
});
const sql = sequelize.getQueryInterface();
const tables = Object.keys(sequelize.models).map((model) => {
const n = sequelize.models[model].getTableName();
return sql.queryGenerator.quoteTable(typeof n === "string" ? n : n.tableName);
});
const flushQuery = `TRUNCATE ${tables.join(", ")}`;
const query = `TRUNCATE ${tables.join(", ")} CASCADE`;
return sequelize.query(query);
export function flushdb() {
return sequelize.query(flushQuery);
}
export const seed = async () => {
@@ -0,0 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should diff a complex document 1`] = `
"<p>This is a second test paragraph. This is a second sentence.</p>
<p>This is a another test paragraph. This is a another sentence.</p>
<ul>
<li>list item 1</li>
<li data-diff-node=\\"del\\" data-operation-index=\\"1\\"><del data-operation-index=\\"1\\">list item 2</del></li></ul>
<pre><code>this is a codeblock
</code></pre><p data-diff-node=\\"ins\\" data-operation-index=\\"3\\"><ins data-operation-index=\\"3\\">This is a new paragraph.</ins></p>
<div class=\\"notice notice-info\\">
<p>This is an info block</p>
</div>
<p><span class=\\"placeholder\\">This is a placeholder</span></p>
<p><span class=\\"highlight\\">this is a highlight</span></p>
<ul>
<li class=\\"checkbox-list-item\\"><span class=\\"checkbox checked\\">[<del data-operation-index=\\"5\\"> ]</del><ins data-operation-index=\\"5\\">x]</ins></span>checklist item 1</li><li class=\\"checkbox-list-item\\"><span class=\\"checkbox checked\\" data-diff-node=\\"ins\\" data-operation-index=\\"7\\"><ins data-operation-index=\\"7\\">[x]</ins></span><ins data-operation-index=\\"7\\">checklist item 2</ins></li>
<li class=\\"checkbox-list-item\\"><span class=\\"checkbox \\">[ ]</span>checklist item <del data-operation-index=\\"9\\">2</del><ins data-operation-index=\\"9\\">3</ins></li><li class=\\"checkbox-list-item\\"><span class=\\"checkbox \\" data-diff-node=\\"ins\\" data-operation-index=\\"9\\"><ins data-operation-index=\\"9\\">[ ]</ins></span><ins data-operation-index=\\"9\\">checklist item 4</ins></li>
<li class=\\"checkbox-list-item\\"><span class=\\"checkbox checked\\">[x]</span>checklist item <del data-operation-index=\\"11\\">3</del><ins data-operation-index=\\"11\\">5</ins></li>
</ul>"
`;
exports[`should return everything inserted when previously empty 1`] = `
"<h1 data-diff-node=\\"ins\\" data-operation-index=\\"0\\"><ins data-operation-index=\\"0\\">Heading 1</ins></h1><h2 data-diff-node=\\"ins\\" data-operation-index=\\"0\\"><ins data-operation-index=\\"0\\">Heading 2</ins></h2><p data-diff-node=\\"ins\\" data-operation-index=\\"0\\"><ins data-operation-index=\\"0\\">This is a test paragraph</ins></p><p data-diff-node=\\"ins\\" data-operation-index=\\"0\\"><ins data-operation-index=\\"0\\">This is a second test paragraph. This is a second sentence.</ins></p><p data-diff-node=\\"ins\\" data-operation-index=\\"0\\"><ins data-operation-index=\\"0\\">This is a another test paragraph. This is a another sentence.</ins></p><ul data-diff-node=\\"ins\\" data-operation-index=\\"0\\"><li data-diff-node=\\"ins\\" data-operation-index=\\"0\\"><ins data-operation-index=\\"0\\">list item 1</ins></li><li data-diff-node=\\"ins\\" data-operation-index=\\"0\\"><ins data-operation-index=\\"0\\">list item 2</ins></li></ul><pre data-diff-node=\\"ins\\" data-operation-index=\\"0\\"><code data-diff-node=\\"ins\\" data-operation-index=\\"0\\"><ins data-operation-index=\\"0\\">this is a codeblock
</ins></code></pre><div class=\\"notice notice-info\\" data-diff-node=\\"ins\\" data-operation-index=\\"0\\"><p data-diff-node=\\"ins\\" data-operation-index=\\"0\\"><ins data-operation-index=\\"0\\">This is an info block</ins></p></div><p data-diff-node=\\"ins\\" data-operation-index=\\"0\\"><span class=\\"placeholder\\" data-diff-node=\\"ins\\" data-operation-index=\\"0\\"><ins data-operation-index=\\"0\\">This is a placeholder</ins></span></p><p data-diff-node=\\"ins\\" data-operation-index=\\"0\\"><span class=\\"highlight\\" data-diff-node=\\"ins\\" data-operation-index=\\"0\\"><ins data-operation-index=\\"0\\">this is a highlight</ins></span></p><ul data-diff-node=\\"ins\\" data-operation-index=\\"0\\"><li class=\\"checkbox-list-item\\" data-diff-node=\\"ins\\" data-operation-index=\\"0\\"><span class=\\"checkbox \\" data-diff-node=\\"ins\\" data-operation-index=\\"0\\"><ins data-operation-index=\\"0\\">[ ]</ins></span><ins data-operation-index=\\"0\\">checklist item 1</ins></li><li class=\\"checkbox-list-item\\" data-diff-node=\\"ins\\" data-operation-index=\\"0\\"><span class=\\"checkbox \\" data-diff-node=\\"ins\\" data-operation-index=\\"0\\"><ins data-operation-index=\\"0\\">[ ]</ins></span><ins data-operation-index=\\"0\\">checklist item 2</ins></li><li class=\\"checkbox-list-item\\" data-diff-node=\\"ins\\" data-operation-index=\\"0\\"><span class=\\"checkbox checked\\" data-diff-node=\\"ins\\" data-operation-index=\\"0\\"><ins data-operation-index=\\"0\\">[x]</ins></span><ins data-operation-index=\\"0\\">checklist item 3</ins></li></ul><p data-diff-node=\\"ins\\" data-operation-index=\\"0\\"><ins data-operation-index=\\"0\\">same on both sides</ins></p><p data-diff-node=\\"ins\\" data-operation-index=\\"0\\"><ins data-operation-index=\\"0\\">same on both sides</ins></p><p data-diff-node=\\"ins\\" data-operation-index=\\"0\\"><ins data-operation-index=\\"0\\">same on both sides</ins></p>"
`;
+57
View File
@@ -0,0 +1,57 @@
// @flow
import { findIndex, findLastIndex } from "lodash";
import diff from "node-htmldiff";
import { renderToHtml } from "rich-markdown-editor";
export default function markdownDiff(
before: string,
after: string,
fullDiff: boolean = false,
buffer: number = 1
) {
// The basic idea here is to first render the Markdown to HTML, then diff the
// HTML - both sides will have valid HTML so we should have a valid diff as well
const beforeHtml = renderToHtml(before);
const afterHtml = renderToHtml(after);
const diffHtml = diff(beforeHtml, afterHtml);
if (fullDiff) {
return diffHtml;
}
if (before === after) {
return "";
}
// Split diff at paragraphs and find the first and last changed tags
// so we can chop around paragraphs rather than return the entire document.
//
// In an ideal world we'd use an AST here and parse that rather than be doing
// operations on strings. I hope this can be revisted in the future with an
// improved diffing library.
const newParagraph = /(?:^|\n)<p>/;
let lines = diffHtml.split(newParagraph);
const firstChangedLineIndex = findIndex(
lines,
(value) => value.includes("<ins ") || value.includes("<del ")
);
const lastChangedLineIndex = findLastIndex(
lines,
(value) => value.includes("</ins>") || value.includes("</del>")
);
const start = Math.max(0, firstChangedLineIndex - buffer);
const end = Math.min(lines.length, lastChangedLineIndex + buffer);
lines = lines.slice(start, end);
if (!lines.length) {
return "";
}
return [start > 0 ? "" : undefined, ...lines]
.filter((x) => x !== undefined)
.join("\n<p>")
.trim();
}
+55
View File
@@ -0,0 +1,55 @@
// @flow
import fs from "fs";
import path from "path";
import markdownDiff from "./markdownDiff";
it("should diff a complex document", async () => {
const before = await fs.promises.readFile(
path.resolve(process.cwd(), "server", "test", "fixtures", "complex.md"),
"utf8"
);
const after = await fs.promises.readFile(
path.resolve(
process.cwd(),
"server",
"test",
"fixtures",
"complexModified.md"
),
"utf8"
);
const diff = markdownDiff(before, after);
expect(diff).toMatchSnapshot();
});
it("should return empty string when both sides are empty", () => {
const diff = markdownDiff("", "");
expect(diff).toEqual("");
});
it("should return everything inserted when previously empty", async () => {
const content = await fs.promises.readFile(
path.resolve(process.cwd(), "server", "test", "fixtures", "complex.md"),
"utf8"
);
const diff = markdownDiff("", content);
expect(diff).toMatchSnapshot();
});
it("should return empty for changed nodes", async () => {
// Note: This isn't ideal behavior, but it is current behavior. If the diffing
// library is improved then we could potentially render the old + new heading
// with ins/del tags as appropriate.
const diff = markdownDiff("# Heading", "## Heading");
expect(diff).toEqual("");
});
it("should return deleted nodes", async () => {
const diff = markdownDiff("![caption](/image.png)", "");
expect(diff).toEqual(
'<p><del data-operation-index="0"><img src="/image.png" alt="caption"></del></p>'
);
});
+1 -1
View File
@@ -1,5 +1,5 @@
// @flow
const attachmentRegex = /!\[.*?\]\(\/api\/attachments\.redirect\?id=(?<id>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\)/gi;
const attachmentRegex = /\/api\/attachments\.redirect\?id=(?<id>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/gi;
export default function parseAttachmentIds(text: any): string[] {
return [...text.matchAll(attachmentRegex)].map(
+95
View File
@@ -0,0 +1,95 @@
// @flow
import { expect } from "@jest/globals";
import { v4 as uuidv4 } from "uuid";
import parseAttachmentIds from "./parseAttachmentIds";
it("should return an empty array with no matches", () => {
expect(parseAttachmentIds(`some random text`).length).toBe(0);
});
it("should not return orphaned UUID's", () => {
const uuid = uuidv4();
expect(
parseAttachmentIds(`some random text with a uuid ${uuid}
![caption](/images/${uuid}.png)`).length
).toBe(0);
});
it("should parse attachment ID from markdown", () => {
const uuid = uuidv4();
const results = parseAttachmentIds(
`![caption text](/api/attachments.redirect?id=${uuid})`
);
expect(results.length).toBe(1);
expect(results[0]).toBe(uuid);
});
it("should parse attachment ID from markdown with additional query params", () => {
const uuid = uuidv4();
const results = parseAttachmentIds(
`![caption text](/api/attachments.redirect?id=${uuid}&size=2)`
);
expect(results.length).toBe(1);
expect(results[0]).toBe(uuid);
});
it("should parse attachment ID from markdown with fully qualified url", () => {
const uuid = uuidv4();
const results = parseAttachmentIds(
`![caption text](${process.env.URL}/api/attachments.redirect?id=${uuid})`
);
expect(process.env.URL).toBeTruthy();
expect(results.length).toBe(1);
expect(results[0]).toBe(uuid);
});
it("should parse attachment ID from markdown with title", () => {
const uuid = uuidv4();
const results = parseAttachmentIds(
`![caption text](/api/attachments.redirect?id=${uuid} "align-left")`
);
expect(results.length).toBe(1);
expect(results[0]).toBe(uuid);
});
it("should parse multiple attachment IDs from markdown", () => {
const uuid = uuidv4();
const uuid2 = uuidv4();
const results = parseAttachmentIds(
`![caption text](/api/attachments.redirect?id=${uuid})
some text
![another caption](/api/attachments.redirect?id=${uuid2})`
);
expect(results.length).toBe(2);
expect(results[0]).toBe(uuid);
expect(results[1]).toBe(uuid2);
});
it("should parse attachment ID from html", () => {
const uuid = uuidv4();
const results = parseAttachmentIds(
`<img src="/api/attachments.redirect?id=${uuid}" />`
);
expect(results.length).toBe(1);
expect(results[0]).toBe(uuid);
});
it("should parse attachment ID from html with fully qualified url", () => {
const uuid = uuidv4();
const results = parseAttachmentIds(
`<img src="${process.env.URL}/api/attachments.redirect?id=${uuid}" />`
);
expect(process.env.URL).toBeTruthy();
expect(results.length).toBe(1);
expect(results[0]).toBe(uuid);
});
+2 -2
View File
@@ -163,13 +163,13 @@ export const deleteFromS3 = (key: string) => {
.promise();
};
export const getSignedImageUrl = async (key: string) => {
export const getSignedImageUrl = async (key: string, expires: number = 60) => {
const isDocker = process.env.AWS_S3_UPLOAD_BUCKET_URL.match(/http:\/\/s3:/);
const params = {
Bucket: AWS_S3_UPLOAD_BUCKET_NAME,
Key: key,
Expires: 60,
Expires: expires,
};
return isDocker
-4
View File
@@ -1,4 +0,0 @@
/* eslint-disable */
import localStorage from './__mocks__/localStorage';
global.localStorage = localStorage;
+1
View File
@@ -8,6 +8,7 @@ import { initReactI18next } from "react-i18next";
export const languageOptions = [
{ label: "English (US)", value: "en_US" },
{ label: "简体中文 (Chinese, Simplified)", value: "zh_CN" },
{ label: "繁體中文 (Chinese, Traditional)", value: "zh_TW" },
{ label: "Deutsch (Deutschland)", value: "de_DE" },
{ label: "Español (España)", value: "es_ES" },
{ label: "Français (France)", value: "fr_FR" },
+13 -1
View File
@@ -109,6 +109,7 @@
"Dismiss": "Ablehnen",
"Keyboard shortcuts": "Tastaturkürzel",
"Back": "Zurück",
"Collections could not be loaded, please reload the app": "Sammlungen konnten nicht geladen werden, bitte lade die App neu",
"New collection": "Neue Sammlung",
"Collections": "Sammlungen",
"Untitled": "Ohne Titel",
@@ -180,12 +181,14 @@
"Create template": "Vorlage erstellen",
"Duplicate": "Duplizieren",
"Unpublish": "Nicht veröffentlichen",
"Permanently delete": "Permanently delete",
"Move": "Verschieben",
"History": "Verlauf",
"Download": "Herunterladen",
"Print": "Drucken",
"Move {{ documentName }}": "Verschiebe {{ documentName }}",
"Delete {{ documentName }}": "{{ documentName }} löschen",
"Permanently delete {{ documentName }}": "Permanently delete {{ documentName }}",
"Edit group": "Gruppe bearbeiten",
"Delete group": "Gruppe löschen",
"Group options": "Gruppen-Einstellungen",
@@ -215,6 +218,8 @@
"Revoke invite": "Einladung widerrufen",
"Activate account": "Konto aktivieren",
"Suspend account": "Konto sperren",
"API token created": "API token created",
"Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".": "Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".",
"Documents": "Dokumente",
"The document archive is empty at the moment.": "Das Dokumentenarchiv ist momentan leer.",
"Search in collection": "Suche in Sammlung",
@@ -308,6 +313,9 @@
"Deleting": "Wird gelöscht",
"Im sure  Delete": "Ich bin mir sicher  Löschen",
"Archiving": "Wird archiviert",
"Couldnt create the document, try again?": "Dokument konnte nicht erstellt werden. Erneut versuchen?",
"Document permanently deleted": "Document permanently deleted",
"Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.": "Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.",
"Search documents": "Dokumente durchsuchen",
"No documents found for your filters.": "Keine Dokumente anhand Ihre Filter gefunden.",
"Youve not got any drafts at the moment.": "Sie haben im Moment keine Entwürfe.",
@@ -384,8 +392,8 @@
"Active": "Aktiv",
"Everyone": "Alle",
"Admins": "Admins",
"Groups can be used to organize and manage the people on your team.": "Gruppen können verwendet werden, um die Personen in Ihrem Team zu organisieren und zu verwalten.",
"New group": "Neue Gruppe",
"Groups can be used to organize and manage the people on your team.": "Gruppen können verwendet werden, um die Personen in Ihrem Team zu organisieren und zu verwalten.",
"All groups": "Alle Gruppen",
"No groups have been created yet": "Es wurden noch keine Gruppen erstellt",
"Import started": "Import gestartet",
@@ -426,6 +434,10 @@
"Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.": "Verbinden Sie Outline Kollektionen mit Slack-Kanälen, für automatische Nachrichten, wenn Dokumente veröffentlicht oder aktualisiert werden.",
"Connected to the <em>{{ channelName }}</em> channel": "Verbunden mit dem <em>{{ channelName }}</em> Kanal",
"Connect": "Verbinden",
"New token": "New token",
"You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.": "You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.",
"Tokens": "Tokens",
"Create a token": "Create a token",
"Youve not starred any documents yet.": "Keine Favoriten.",
"There are no templates just yet.": "Es gibt noch keine Vorlagen.",
"You can create templates to help your team create consistent and accurate documentation.": "Du kannst Vorlagen erstellen, um deinem Team zu helfen, eine konsistente und genaue Dokumentation zu schaffen.",
+5 -1
View File
@@ -152,12 +152,12 @@
"Path to document": "Path to document",
"Group member options": "Group member options",
"Remove": "Remove",
"Collection": "Collection",
"New document": "New document",
"Import document": "Import document",
"Edit": "Edit",
"Permissions": "Permissions",
"Delete": "Delete",
"Collection": "Collection",
"Collection permissions": "Collection permissions",
"Edit collection": "Edit collection",
"Delete collection": "Delete collection",
@@ -181,12 +181,14 @@
"Create template": "Create template",
"Duplicate": "Duplicate",
"Unpublish": "Unpublish",
"Permanently delete": "Permanently delete",
"Move": "Move",
"History": "History",
"Download": "Download",
"Print": "Print",
"Move {{ documentName }}": "Move {{ documentName }}",
"Delete {{ documentName }}": "Delete {{ documentName }}",
"Permanently delete {{ documentName }}": "Permanently delete {{ documentName }}",
"Edit group": "Edit group",
"Delete group": "Delete group",
"Group options": "Group options",
@@ -312,6 +314,8 @@
"Im sure  Delete": "Im sure  Delete",
"Archiving": "Archiving",
"Couldnt create the document, try again?": "Couldnt create the document, try again?",
"Document permanently deleted": "Document permanently deleted",
"Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.": "Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.",
"Search documents": "Search documents",
"No documents found for your filters.": "No documents found for your filters.",
"Youve not got any drafts at the moment.": "Youve not got any drafts at the moment.",
+13 -1
View File
@@ -109,6 +109,7 @@
"Dismiss": "Descartar",
"Keyboard shortcuts": "Atajos del teclado",
"Back": "Atras",
"Collections could not be loaded, please reload the app": "Collections could not be loaded, please reload the app",
"New collection": "Nueva colección",
"Collections": "Colecciones",
"Untitled": "Sin título",
@@ -180,12 +181,14 @@
"Create template": "Crear plantilla",
"Duplicate": "Duplicar",
"Unpublish": "Cancelar publicación",
"Permanently delete": "Permanently delete",
"Move": "Mover",
"History": "Historial",
"Download": "Descargar",
"Print": "Imprimir",
"Move {{ documentName }}": "Mover {{ documentName }}",
"Delete {{ documentName }}": "Eliminar {{ documentName }}",
"Permanently delete {{ documentName }}": "Permanently delete {{ documentName }}",
"Edit group": "Editar grupo",
"Delete group": "Eliminar grupo",
"Group options": "Opciones de grupo",
@@ -215,6 +218,8 @@
"Revoke invite": "Revocar Invitación",
"Activate account": "Activar cuenta",
"Suspend account": "Suspender cuenta",
"API token created": "API token created",
"Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".": "Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".",
"Documents": "Documentos",
"The document archive is empty at the moment.": "El archivo de documento está vacío en este momento.",
"Search in collection": "Buscar en colección",
@@ -308,6 +313,9 @@
"Deleting": "Deleting",
"Im sure  Delete": "Im sure  Delete",
"Archiving": "Archiving",
"Couldnt create the document, try again?": "Couldnt create the document, try again?",
"Document permanently deleted": "Document permanently deleted",
"Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.": "Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.",
"Search documents": "Search documents",
"No documents found for your filters.": "No se encontraron documentos para sus filtros.",
"Youve not got any drafts at the moment.": "No tienes borradores en este momento.",
@@ -384,8 +392,8 @@
"Active": "Active",
"Everyone": "Everyone",
"Admins": "Admins",
"Groups can be used to organize and manage the people on your team.": "Groups can be used to organize and manage the people on your team.",
"New group": "New group",
"Groups can be used to organize and manage the people on your team.": "Groups can be used to organize and manage the people on your team.",
"All groups": "All groups",
"No groups have been created yet": "No groups have been created yet",
"Import started": "Import started",
@@ -426,6 +434,10 @@
"Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.": "Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.",
"Connected to the <em>{{ channelName }}</em> channel": "Connected to the <em>{{ channelName }}</em> channel",
"Connect": "Connect",
"New token": "New token",
"You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.": "You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.",
"Tokens": "Tokens",
"Create a token": "Create a token",
"Youve not starred any documents yet.": "Todavía no has marcado documentos como favoritos.",
"There are no templates just yet.": "There are no templates just yet.",
"You can create templates to help your team create consistent and accurate documentation.": "You can create templates to help your team create consistent and accurate documentation.",
+450
View File
@@ -0,0 +1,450 @@
{
"currently editing": "در حال ویرایش",
"currently viewing": "در حال مشاهده",
"previously edited": "قبلاً ویرایش شده",
"You": "شما",
"Viewers": "مشاهده کنندگان",
"Sorry, an error occurred saving the collection": "متاسفانه خطایی در ذخیره‌سازی مجموعه رخ داد",
"Add a description": "توضیحاتی اضافه کنید",
"Collapse": "جمع کردن",
"Expand": "باز کردن",
"Submenu": "زیرمنو",
"Trash": "زباله‌دان",
"Archive": "آرشیو",
"Drafts": "پیش‌نویس‌ها",
"Templates": "قالب‌ها",
"Deleted Collection": "مجموعه‌های حذف شده",
"New": "جدید",
"Only visible to you": "تنها قابل مشاهده برای شما",
"Draft": "پیش‌نویس",
"Template": "قالب",
"New doc": "سند جدید",
"deleted": "حذف شده",
"archived": "آرشیو شده",
"created": "ایجاد شده",
"published": "منتشر شده",
"saved": "ذخیره شده",
"updated": "به‌روز شده",
"Never viewed": "هرگز مشاهده نشده",
"Viewed": "مشاهده شده",
"in": "در",
"nested document": "زیرسند",
"nested document_plural": "زیرسندها",
"Viewed by": "مشاهده شده توسط",
"only you": "فقط شما",
"person": "نفر",
"people": "افراد",
"Currently editing": "در حال ویرایش",
"Currently viewing": "در حال مشاهده",
"Viewed {{ timeAgo }} ago": "مشاهده شده در {{ timeAgo }} پیش",
"Insert column after": "درج ستون پس از",
"Insert column before": "درج ستون پیش از",
"Insert row after": "درج سطر پس از",
"Insert row before": "درج سطر پیش از",
"Align center": "تراز به مرکز",
"Align left": "تراز به چپ",
"Align right": "تراز به راست",
"Bulleted list": "لیست گلوله‌ای",
"Todo list": "لیست کارها",
"Code block": "بلوک کد",
"Copied to clipboard": "در بریده‌دان کپی شد",
"Code": "کد",
"Create link": "ایجاد پیوند",
"Sorry, an error occurred creating the link": "متاسفانه خطایی در ذخیره‌سازی پیوند رخ داد",
"Create a new doc": "ایجاد سند جدید",
"Delete column": "حذف ستون",
"Delete row": "حذف سطر",
"Delete table": "حذف جدول",
"Delete image": "حذف تصویر",
"Download image": "بارگیری تصویر",
"Float left": "شناور به چپ",
"Float right": "شناور به راست",
"Center large": "مرکز و بزرگ",
"Italic": "مورب",
"Sorry, that link wont work for this embed type": "متاسفانه این پیوند برای این نوع جاسازی کار نمی‌کند",
"Find or create a doc": "یافتن یا ایجاد سند",
"Big heading": "عنوان بزرگ",
"Medium heading": "عنوان متوسط",
"Small heading": "عنوان کوچک",
"Heading": "عنوان",
"Divider": "جدا کننده",
"Image": "تصویر",
"Sorry, an error occurred uploading the image": "متاسفانه خطایی در بارگذاری تصویر رخ داد",
"Info": "اطلاع‌رسانی",
"Info notice": "اخطار اطلاع‌رسانی",
"Link": "پیوند",
"Link copied to clipboard": "پیوند در بریده‌دان کپی شد",
"Highlight": "هایلایت",
"Type '/' to insert": "برای درج کلید / را بزنید",
"Keep typing to filter": "برای جستجو به نوشتن ادامه دهید",
"No results": "بدون نتیجه",
"Open link": "باز کردن پیوند",
"Ordered list": "لیست شماره‌دار",
"Page break": "جداکننده صفحه",
"Paste a link": "جایگذاری پیوند",
"Paste a {{service}} link…": "جایگذاری پیوند {{service}}…",
"Placeholder": "محل نگهدارنده",
"Quote": "نقل قول",
"Remove link": "حذف پیوند",
"Search or paste a link": "جستجو کنید یا یک پیوند جایگذاری کنید",
"Strikethrough": "خط خورده",
"Bold": "پررنگ",
"Subheading": "زیرعنوان",
"Table": "جدول",
"Tip": "نکته",
"Tip notice": "اخطار نکته",
"Warning": "هشدار",
"Warning notice": "اخطار هشدار",
"Icon": "نماد",
"Show menu": "نمایش منو",
"Choose icon": "انتخاب نماد",
"Loading": "بارگذاری",
"Search": "جستجو",
"Default access": "دسترسی پیش‌فرض",
"View and edit": "مشاهده و ویرایش",
"View only": "فقط مشاهده",
"No access": "بدون دسترسی",
"Outline is available in your language {{optionLabel}}, would you like to change?": "سامانه Outline به زبان شما {{optionLabel}} در دسترس است، آیا زبان سامانه تغییر کند؟",
"Change Language": "تغییر زبان",
"Dismiss": "رد کردن",
"Keyboard shortcuts": "میان‌برهای صفحه کلید",
"Back": "بازگشت",
"Collections could not be loaded, please reload the app": "مجموعه‌ها بارگذاری نمی‌شوند، لطفاً برنامه را دوباره بارگیری کنید",
"New collection": "مجموعه جدید",
"Collections": "مجموعه‌ها",
"Untitled": "بدون عنوان",
"Document not supported try Markdown, Plain text, HTML, or Word": "Document not supported try Markdown, Plain text, HTML, or Word",
"Home": "Home",
"Starred": "Starred",
"Settings": "Settings",
"Invite people": "Invite people",
"Create a collection": "Create a collection",
"Return to App": "Back to App",
"Account": "Account",
"Profile": "Profile",
"Notifications": "Notifications",
"API Tokens": "API Tokens",
"Team": "Team",
"Details": "Details",
"Security": "Security",
"Members": "Members",
"Groups": "Groups",
"Share Links": "Share Links",
"Import": "Import",
"Export": "Export",
"Integrations": "Integrations",
"Installation": "Installation",
"Unstar": "Unstar",
"Star": "Star",
"Previous page": "Previous page",
"Next page": "Next page",
"Could not import file": "Could not import file",
"Appearance": "Appearance",
"System": "System",
"Light": "Light",
"Dark": "Dark",
"API documentation": "API documentation",
"Changelog": "Changelog",
"Send us feedback": "Send us feedback",
"Report a bug": "Report a bug",
"Log out": "Log out",
"Show path to document": "Show path to document",
"Path to document": "Path to document",
"Group member options": "Group member options",
"Remove": "حذف",
"Collection": "مجموعه",
"New document": "سند جدید",
"Import document": "وارد کردن سند",
"Edit": "ویرایش",
"Permissions": "دسترسی‌ها",
"Delete": "حذف",
"Collection permissions": "دسترسی‌های مجموعه",
"Edit collection": "ویرایش مجموعه",
"Delete collection": "حذف مجموعه",
"Export collection": "صدور مجموعه",
"Show sort menu": "نمایش منوی مرتب‌سازی",
"Sort in sidebar": "مرتب‌سازی در نوار کناری",
"Alphabetical sort": "مرتب‌سازی الفبایی",
"Manual sort": "مرتب‌سازی دستی",
"Document duplicated": "سند کپی شد",
"Document archived": "سند آرشیو شد",
"Document restored": "سند بازیابی شد",
"Document unpublished": "انتشار سند لغو شد",
"Document options": "گزینه‌های سند",
"Restore": "بازیابی",
"Choose a collection": "انتخاب یک مجموعه",
"Unpin": "برداشتن سنجاق",
"Pin to collection": "سنجاق کردن به مجموعه",
"Enable embeds": "فعال‌سازی جاسازی‌ها",
"Disable embeds": "غیرفعال‌سازی جاسازی‌ها",
"New nested document": "ایجاد زیرسند جدید",
"Create template": "ایجاد قالب",
"Duplicate": "کپی کردن",
"Unpublish": "لغو انتشار",
"Permanently delete": "حذف برای همیشه",
"Move": "انتقال",
"History": "تاریخچه",
"Download": "بارگیری",
"Print": "چاپ",
"Move {{ documentName }}": "انتقال {{ documentName }}",
"Delete {{ documentName }}": "حذف {{ documentName }}",
"Permanently delete {{ documentName }}": "حذف همیشگی {{ documentName }}",
"Edit group": "ویرایش گروه",
"Delete group": "حذف گروه",
"Group options": "گزینه‌های گروه",
"Member options": "گزینه‌های اعضا",
"collection": "مجموعه",
"New child document": "سند فرزند جدید",
"New document in <em>{{ collectionName }}</em>": "سند جدید در <em>{{ collectionName }}</em>",
"New template": "قالب جدید",
"Link copied": "پیوند کپی شد",
"Revision options": "گزینه‌های بازنگری‌ها",
"Restore version": "بازیابی نسخه",
"Copy link": "کپی پیوند",
"Share link revoked": "Share link revoked",
"Share link copied": "Share link copied",
"Share options": "Share options",
"Go to document": "Go to document",
"Revoke link": "Revoke link",
"By {{ author }}": "By {{ author }}",
"Are you sure you want to make {{ userName }} an admin? Admins can modify team and billing information.": "Are you sure you want to make {{ userName }} an admin? Admins can modify team and billing information.",
"Are you sure you want to make {{ userName }} a member?": "Are you sure you want to make {{ userName }} a member?",
"Are you sure you want to make {{ userName }} a read-only viewer? They will not be able to edit any content": "Are you sure you want to make {{ userName }} a read-only viewer? They will not be able to edit any content",
"Are you sure you want to suspend this account? Suspended users will be prevented from logging in.": "Are you sure you want to suspend this account? Suspended users will be prevented from logging in.",
"User options": "User options",
"Make {{ userName }} a member": "Make {{ userName }} a member",
"Make {{ userName }} a viewer": "Make {{ userName }} a viewer",
"Make {{ userName }} an admin…": "Make {{ userName }} an admin…",
"Revoke invite": "Revoke invite",
"Activate account": "Activate account",
"Suspend account": "Suspend account",
"API token created": "API token created",
"Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".": "Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".",
"Documents": "اسناد",
"The document archive is empty at the moment.": "The document archive is empty at the moment.",
"Search in collection": "Search in collection",
"<em>{{ collectionName }}</em> doesnt contain any\n documents yet.": "<em>{{ collectionName }}</em> doesnt contain any\n documents yet.",
"Get started by creating a new one!": "Get started by creating a new one!",
"Create a document": "ایجاد سند",
"Manage permissions": "مدیریت دسترسی‌ها",
"This collection is only visible to those given access": "این مجموعه فقط برای افراد دارای دسترسی قابل مشاهده است",
"Private": "خصوصی",
"Pinned": "سنجاق شده",
"Recently updated": "آخرین به‌روزرسانی",
"Recently published": "آخرین انتشار",
"Least recently updated": "اولین به‌روزرسانی",
"AZ": "الفبایی",
"Drop documents to import": "اسناد را بکشید تا وارد شوند",
"The collection was updated": "مجموعه به‌روز شد",
"You can edit the name and other details at any time, however doing so often might confuse your team mates.": "You can edit the name and other details at any time, however doing so often might confuse your team mates.",
"Name": "Name",
"Alphabetical": "Alphabetical",
"Public document sharing": "Public document sharing",
"When enabled, documents can be shared publicly on the internet.": "When enabled, documents can be shared publicly on the internet.",
"Public sharing is currently disabled in the team security settings.": "Public sharing is currently disabled in the team security settings.",
"Saving": "Saving",
"Save": "Save",
"Collections are for grouping your documents. They work best when organized around a topic or internal team — Product or Engineering for example.": "Collections are for grouping your documents. They work best when organized around a topic or internal team — Product or Engineering for example.",
"This is the default level of access given to team members, you can give specific users or groups more access once the collection is created.": "This is the default level of access given to team members, you can give specific users or groups more access once the collection is created.",
"Creating": "Creating",
"Create": "Create",
"{{ groupName }} was added to the collection": "{{ groupName }} was added to the collection",
"Could not add user": "Could not add user",
"Cant find the group youre looking for?": "Cant find the group youre looking for?",
"Create a group": "Create a group",
"Search by group name": "Search by group name",
"Search groups": "Search groups",
"No groups matching your search": "No groups matching your search",
"No groups left to add": "No groups left to add",
"Add": "Add",
"{{ userName }} was added to the collection": "{{ userName }} was added to the collection",
"Need to add someone whos not yet on the team yet?": "Need to add someone whos not yet on the team yet?",
"Invite people to {{ teamName }}": "Invite people to {{ teamName }}",
"Search by name": "Search by name",
"Search people": "Search people",
"No people matching your search": "No people matching your search",
"No people left to add": "No people left to add",
"Active <1></1> ago": "Active <1></1> ago",
"Never signed in": "Never signed in",
"Invited": "Invited",
"Admin": "Admin",
"{{ userName }} was removed from the collection": "{{ userName }} was removed from the collection",
"Could not remove user": "Could not remove user",
"{{ userName }} permissions were updated": "{{ userName }} permissions were updated",
"Could not update user": "Could not update user",
"The {{ groupName }} group was removed from the collection": "The {{ groupName }} group was removed from the collection",
"Could not remove group": "Could not remove group",
"{{ groupName }} permissions were updated": "{{ groupName }} permissions were updated",
"Default access permissions were updated": "Default access permissions were updated",
"Could not update permissions": "Could not update permissions",
"The <em>{{ collectionName }}</em> collection is private. Team members have no access to it by default.": "The <em>{{ collectionName }}</em> collection is private. Team members have no access to it by default.",
"Team members can view documents in the <em>{{ collectionName }}</em> collection by default.": "Team members can view documents in the <em>{{ collectionName }}</em> collection by default.",
"Team members can view and edit documents in the <em>{{ collectionName }}</em> collection by\n default.": "Team members can view and edit documents in the <em>{{ collectionName }}</em> collection by\n default.",
"Additional access": "Additional access",
"Add groups": "Add groups",
"Add people": "Add people",
"Add specific access for individual groups and team members": "Add specific access for individual groups and team members",
"Add groups to {{ collectionName }}": "Add groups to {{ collectionName }}",
"Add people to {{ collectionName }}": "Add people to {{ collectionName }}",
"Hide contents": "Hide contents",
"Show contents": "Show contents",
"Edit {{noun}}": "Edit {{noun}}",
"Archived": "Archived",
"Save Draft": "Save Draft",
"Done Editing": "Done Editing",
"New from template": "New from template",
"Publish": "Publish",
"Publishing": "Publishing",
"Nested documents": "Nested documents",
"Anyone with the link <1></1>can view this document": "Anyone with the link <1></1>can view this document",
"Share": "Share",
"Share this document": "Share this document",
"This document is shared because the parent <em>{{ documentTitle }}</em> is publicly shared": "This document is shared because the parent <em>{{ documentTitle }}</em> is publicly shared",
"Publish to internet": "Publish to internet",
"Anyone with the link can view this document": "Anyone with the link can view this document",
"Only team members with permission can view": "Only team members with permission can view",
"The shared link was last accessed {{ timeAgo }}.": "The shared link was last accessed {{ timeAgo }}.",
"Share nested documents": "Share nested documents",
"Nested documents are publicly available": "Nested documents are publicly available",
"Nested documents are not shared": "Nested documents are not shared",
"Are you sure you want to delete the <em>{{ documentTitle }}</em> template?": "Are you sure you want to delete the <em>{{ documentTitle }}</em> template?",
"Are you sure about that? Deleting the <em>{{ documentTitle }}</em> document will delete all of its history and any nested documents.": "Are you sure about that? Deleting the <em>{{ documentTitle }}</em> document will delete all of its history and any nested documents.",
"If youd like the option of referencing or restoring the {{noun}} in the future, consider archiving it instead.": "If youd like the option of referencing or restoring the {{noun}} in the future, consider archiving it instead.",
"Deleting": "Deleting",
"Im sure  Delete": "Im sure  Delete",
"Archiving": "Archiving",
"Couldnt create the document, try again?": "Couldnt create the document, try again?",
"Document permanently deleted": "Document permanently deleted",
"Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.": "Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.",
"Search documents": "Search documents",
"No documents found for your filters.": "No documents found for your filters.",
"Youve not got any drafts at the moment.": "Youve not got any drafts at the moment.",
"Not found": "Not found",
"We were unable to find the page youre looking for. Go to the <2>homepage</2>?": "We were unable to find the page youre looking for. Go to the <2>homepage</2>?",
"Offline": "Offline",
"We were unable to load the document while offline.": "We were unable to load the document while offline.",
"Your account has been suspended": "Your account has been suspended",
"A team admin (<em>{{ suspendedContactEmail }}</em>) has suspended your account. To re-activate your account, please reach out to them directly.": "A team admin (<em>{{ suspendedContactEmail }}</em>) has suspended your account. To re-activate your account, please reach out to them directly.",
"{{userName}} was added to the group": "{{userName}} was added to the group",
"Add team members below to give them access to the group. Need to add someone whos not yet on the team yet?": "Add team members below to give them access to the group. Need to add someone whos not yet on the team yet?",
"Invite them to {{teamName}}": "Invite them to {{teamName}}",
"{{userName}} was removed from the group": "{{userName}} was removed from the group",
"This group has no members.": "This group has no members.",
"Recently viewed": "Recently viewed",
"Created by me": "Created by me",
"Navigation": "Navigation",
"Edit current document": "Edit current document",
"Move current document": "Move current document",
"Jump to search": "Jump to search",
"Jump to home": "Jump to home",
"Table of contents": "Table of contents",
"Toggle navigation": "Toggle navigation",
"Focus search input": "Focus search input",
"Open this guide": "Open this guide",
"Save document and exit": "Save document and exit",
"Publish document and exit": "Publish document and exit",
"Save document": "Save document",
"Cancel editing": "Cancel editing",
"Formatting": "Formatting",
"Paragraph": "Paragraph",
"Large header": "Large header",
"Medium header": "Medium header",
"Small header": "Small header",
"Underline": "Underline",
"Undo": "Undo",
"Redo": "Redo",
"Lists": "Lists",
"Indent list item": "Indent list item",
"Outdent list item": "Outdent list item",
"Move list item up": "Move list item up",
"Move list item down": "Move list item down",
"Numbered list": "Numbered list",
"Blockquote": "Blockquote",
"Horizontal divider": "Horizontal divider",
"Inline code": "Inline code",
"Any collection": "Any collection",
"Any time": "Any time",
"Past day": "Past day",
"Past week": "Past week",
"Past month": "Past month",
"Past year": "Past year",
"Active documents": "Active documents",
"Documents in collections you are able to access": "Documents in collections you are able to access",
"All documents": "All documents",
"Include documents that are in the archive": "Include documents that are in the archive",
"Any author": "Any author",
"Author": "Author",
"Not Found": "Not Found",
"We were unable to find the page youre looking for.": "We were unable to find the page youre looking for.",
"Use the <em>{{ meta }}+K</em> shortcut to search from anywhere in your knowledge base": "Use the <em>{{ meta }}+K</em> shortcut to search from anywhere in your knowledge base",
"No documents found for your search filters. <1></1>": "No documents found for your search filters. <1></1>",
"Create a new document?": "Create a new document?",
"Clear filters": "Clear filters",
"Email": "Email",
"Last active": "Last active",
"Role": "Role",
"Viewer": "Viewer",
"Suspended": "Suspended",
"Shared": "Shared",
"by {{ name }}": "by {{ name }}",
"Last accessed": "Last accessed",
"Add to Slack": "Add to Slack",
"Active": "Active",
"Everyone": "Everyone",
"Admins": "Admins",
"New group": "New group",
"Groups can be used to organize and manage the people on your team.": "Groups can be used to organize and manage the people on your team.",
"All groups": "All groups",
"No groups have been created yet": "No groups have been created yet",
"Import started": "Import started",
"Export in progress…": "Export in progress…",
"It is possible to import a zip file of folders and Markdown files previously exported from an Outline instance. Support will soon be added for importing from other services.": "It is possible to import a zip file of folders and Markdown files previously exported from an Outline instance. Support will soon be added for importing from other services.",
"Your file has been uploaded and the import is currently being processed, you can safely leave this page while it completes.": "Your file has been uploaded and the import is currently being processed, you can safely leave this page while it completes.",
"Sorry, the file <em>{{ fileName }}</em> is missing valid collections or documents.": "Sorry, the file <em>{{ fileName }}</em> is missing valid collections or documents.",
"<em>{{ fileName }}</em> looks good, the following collections and their documents will be imported:": "<em>{{ fileName }}</em> looks good, the following collections and their documents will be imported:",
"Uploading": "Uploading",
"Confirm & Import": "Confirm & Import",
"Choose File": "Choose File",
"A full export might take some time, consider exporting a single document or collection if possible. Well put together a zip of all your documents in Markdown format and email it to <em>{{ userEmail }}</em>.": "A full export might take some time, consider exporting a single document or collection if possible. Well put together a zip of all your documents in Markdown format and email it to <em>{{ userEmail }}</em>.",
"Export Requested": "Export Requested",
"Requesting Export": "Requesting Export",
"Export Data": "Export Data",
"Everyone that has signed into Outline appears here. Its possible that there are other users who have access through {team.signinMethods} but havent signed in yet.": "Everyone that has signed into Outline appears here. Its possible that there are other users who have access through {team.signinMethods} but havent signed in yet.",
"Filter": "Filter",
"Profile saved": "Profile saved",
"Profile picture updated": "Profile picture updated",
"Unable to upload new profile picture": "Unable to upload new profile picture",
"Photo": "Photo",
"Upload": "Upload",
"Full name": "Full name",
"Language": "Language",
"Please note that translations are currently in early access.<1></1>Community contributions are accepted though our <4>translation portal</4>": "Please note that translations are currently in early access.<1></1>Community contributions are accepted though our <4>translation portal</4>",
"Delete Account": "Delete Account",
"You may delete your account at any time, note that this is unrecoverable": "You may delete your account at any time, note that this is unrecoverable",
"Delete account": "Delete account",
"Documents that have been shared are listed below. Anyone that has the public link can access a read-only version of the document until the link has been revoked.": "Documents that have been shared are listed below. Anyone that has the public link can access a read-only version of the document until the link has been revoked.",
"Sharing is currently disabled.": "Sharing is currently disabled.",
"You can globally enable and disable public document sharing in the <em>security settings</em>.": "You can globally enable and disable public document sharing in the <em>security settings</em>.",
"Shared documents": "Shared documents",
"No share links, yet.": "No share links, yet.",
"Whoops, you need to accept the permissions in Slack to connect Outline to your team. Try again?": "Whoops, you need to accept the permissions in Slack to connect Outline to your team. Try again?",
"Something went wrong while authenticating your request. Please try logging in again?": "Something went wrong while authenticating your request. Please try logging in again?",
"Get rich previews of Outline links shared in Slack and use the <em>{{ command }}</em> slash command to search for documents without leaving your chat.": "Get rich previews of Outline links shared in Slack and use the <em>{{ command }}</em> slash command to search for documents without leaving your chat.",
"Disconnect": "Disconnect",
"Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.": "Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.",
"Connected to the <em>{{ channelName }}</em> channel": "Connected to the <em>{{ channelName }}</em> channel",
"Connect": "Connect",
"New token": "New token",
"You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.": "You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.",
"Tokens": "Tokens",
"Create a token": "Create a token",
"Youve not starred any documents yet.": "Youve not starred any documents yet.",
"There are no templates just yet.": "There are no templates just yet.",
"You can create templates to help your team create consistent and accurate documentation.": "You can create templates to help your team create consistent and accurate documentation.",
"Trash is empty at the moment.": "Trash is empty at the moment.",
"You joined": "You joined",
"Joined": "Joined",
"{{ time }} ago.": "{{ time }} ago.",
"Edit Profile": "Edit Profile",
"{{ userName }} hasnt updated any documents yet.": "{{ userName }} hasnt updated any documents yet."
}
+74 -62
View File
@@ -1,9 +1,9 @@
{
"currently editing": "en train d'éditer",
"currently viewing": "en train de consulter",
"previously edited": "previously edited",
"previously edited": "modifié précédemment",
"You": "Vous",
"Viewers": "Viewers",
"Viewers": "Observateurs",
"Sorry, an error occurred saving the collection": "Désolé, une erreur s'est produite lors de l'enregistrement de la collection",
"Add a description": "Ajouter une description",
"Collapse": "Replier",
@@ -30,13 +30,13 @@
"in": "dans",
"nested document": "document imbriqué",
"nested document_plural": "documents imbriqués",
"Viewed by": "Viewed by",
"only you": "only you",
"person": "person",
"people": "people",
"Currently editing": "Currently editing",
"Currently viewing": "Currently viewing",
"Viewed {{ timeAgo }} ago": "Viewed {{ timeAgo }} ago",
"Viewed by": "Consulté par",
"only you": "moi uniquement",
"person": "personne",
"people": "personnes",
"Currently editing": "En cours de modification",
"Currently viewing": "En train de consulter",
"Viewed {{ timeAgo }} ago": "Vu il y a {{ timeAgo }}",
"Insert column after": "Insérer une colonne après",
"Insert column before": "Insérer une colonne avant",
"Insert row after": "Insérer une ligne après",
@@ -55,10 +55,10 @@
"Delete column": "Supprimer la colonne",
"Delete row": "Supprimer la ligne",
"Delete table": "Supprimer ce tableau",
"Delete image": "Delete image",
"Download image": "Download image",
"Float left": "Float left",
"Float right": "Float right",
"Delete image": "Supprimer l'image",
"Download image": "Télécharger limage",
"Float left": "Aligner à gauche",
"Float right": "Aligner à droite",
"Center large": "Center large",
"Italic": "Italique",
"Sorry, that link wont work for this embed type": "Désolé, ce lien ne fonctionne pas pour ce type d'intégration",
@@ -80,7 +80,7 @@
"No results": "Aucun résultats",
"Open link": "Ouvrir le lien",
"Ordered list": "Liste ordonnée",
"Page break": "Page break",
"Page break": "Saut de page",
"Paste a link": "Coller un lien",
"Paste a {{service}} link…": "Collez un lien {{service}}…",
"Placeholder": "Indication",
@@ -109,10 +109,11 @@
"Dismiss": "Rejeter",
"Keyboard shortcuts": "Raccourcis clavier",
"Back": "Retour",
"Collections could not be loaded, please reload the app": "Les collections n'ont pas pu être chargées, veuillez recharger l'application",
"New collection": "Nouvelle collection",
"Collections": "Collections",
"Untitled": "Sans titre",
"Document not supported try Markdown, Plain text, HTML, or Word": "Document not supported try Markdown, Plain text, HTML, or Word",
"Document not supported try Markdown, Plain text, HTML, or Word": "Document non pris en charge - essayez un format Markdown, Text, HTML ou Word",
"Home": "Accueil",
"Starred": "Favoris",
"Settings": "Réglages",
@@ -135,9 +136,9 @@
"Installation": "Installation",
"Unstar": "Enlever des favoris",
"Star": "Ajouter aux favoris",
"Previous page": "Previous page",
"Next page": "Next page",
"Could not import file": "Could not import file",
"Previous page": "Page précédente",
"Next page": "Page suivante",
"Could not import file": "Le fichier n'a pas pu être importé",
"Appearance": "Affichage",
"System": "Système",
"Light": "Clair",
@@ -180,12 +181,14 @@
"Create template": "Créer un modèle",
"Duplicate": "Dupliquer",
"Unpublish": "Dépublier",
"Permanently delete": "Supprimer définitivement",
"Move": "Déplacer",
"History": "Historique",
"Download": "Télécharger",
"Print": "Imprimer",
"Move {{ documentName }}": "Déplacer {{ documentName }}",
"Delete {{ documentName }}": "Supprimer {{ documentName }}",
"Permanently delete {{ documentName }}": "Supprimer définitivement {{ documentName }}",
"Edit group": "Modifier le groupe",
"Delete group": "Supprimer le groupe",
"Group options": "Options de groupe",
@@ -206,15 +209,17 @@
"By {{ author }}": "Par {{ author }}",
"Are you sure you want to make {{ userName }} an admin? Admins can modify team and billing information.": "Voulez-vous vraiment faire de {{ userName }} un administrateur ? Les administrateurs peuvent modifier les informations relatives à l'équipe et à la facturation.",
"Are you sure you want to make {{ userName }} a member?": "Êtes-vous sûr de vouloir faire de {{ userName }} un membre ?",
"Are you sure you want to make {{ userName }} a read-only viewer? They will not be able to edit any content": "Are you sure you want to make {{ userName }} a read-only viewer? They will not be able to edit any content",
"Are you sure you want to make {{ userName }} a read-only viewer? They will not be able to edit any content": "Êtes-vous sûr de vouloir faire de {{ userName }} un utilisateur en lecture seule ? Il ne sera pas en mesure de modifier le moindre contenu",
"Are you sure you want to suspend this account? Suspended users will be prevented from logging in.": "Voulez-vous vraiment suspendre ce compte ? Les utilisateurs suspendus ne pourront plus se connecter.",
"User options": "Options utilisateur",
"Make {{ userName }} a member": "Make {{ userName }} a member",
"Make {{ userName }} a viewer": "Make {{ userName }} a viewer",
"Make {{ userName }} a member": "Faire de {{ userName }} un membre",
"Make {{ userName }} a viewer": "Faire de {{ userName }} un lecteur",
"Make {{ userName }} an admin…": "Faire de {{ userName }} un administrateur…",
"Revoke invite": "Révoquer l'invitation",
"Activate account": "Activer le compte",
"Suspend account": "Suspendre le compte",
"API token created": "Jeton d'API créé",
"Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".": "Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".",
"Documents": "Documents",
"The document archive is empty at the moment.": "L'archive du document est vide pour le moment.",
"Search in collection": "Rechercher dans la collection",
@@ -229,7 +234,7 @@
"Recently published": "Récemment publiés",
"Least recently updated": "Moins récemment modifiés",
"AZ": "A Z",
"Drop documents to import": "Drop documents to import",
"Drop documents to import": "Déposer les documents à importer",
"The collection was updated": "La collection a été mise à jour",
"You can edit the name and other details at any time, however doing so often might confuse your team mates.": "Vous pouvez modifier le nom et autres détails à tout moment, mais le faire souvent peut perturber les membres de votre équipe.",
"Name": "Nom",
@@ -266,17 +271,17 @@
"{{ userName }} was removed from the collection": "{{ userName }} was removed from the collection",
"Could not remove user": "Impossible de supprimer l'utilisateur",
"{{ userName }} permissions were updated": "{{ userName }} permissions were updated",
"Could not update user": "Could not update user",
"The {{ groupName }} group was removed from the collection": "The {{ groupName }} group was removed from the collection",
"Could not remove group": "Could not remove group",
"{{ groupName }} permissions were updated": "{{ groupName }} permissions were updated",
"Default access permissions were updated": "Default access permissions were updated",
"Could not update user": "Impossible de mettre à jour l'utilisateur",
"The {{ groupName }} group was removed from the collection": "Le groupe {{ groupName }} a été retiré de la collection",
"Could not remove group": "Impossible de supprimer le groupe",
"{{ groupName }} permissions were updated": "Les permissions de {{ groupName }} ont été mises à jour",
"Default access permissions were updated": "Les permissions d'accès par défaut ont été mises à jour",
"Could not update permissions": "Could not update permissions",
"The <em>{{ collectionName }}</em> collection is private. Team members have no access to it by default.": "The <em>{{ collectionName }}</em> collection is private. Team members have no access to it by default.",
"Team members can view documents in the <em>{{ collectionName }}</em> collection by default.": "Team members can view documents in the <em>{{ collectionName }}</em> collection by default.",
"Team members can view and edit documents in the <em>{{ collectionName }}</em> collection by\n default.": "Team members can view and edit documents in the <em>{{ collectionName }}</em> collection by\n default.",
"Additional access": "Additional access",
"Add groups": "Add groups",
"Additional access": "Accès supplémentaire",
"Add groups": "Ajouter des groupes",
"Add people": "Ajouter quelqu'un",
"Add specific access for individual groups and team members": "Add specific access for individual groups and team members",
"Add groups to {{ collectionName }}": "Add groups to {{ collectionName }}",
@@ -290,12 +295,12 @@
"New from template": "Nouveau à partir d'un modèle",
"Publish": "Publier",
"Publishing": "Publication en cours",
"Nested documents": "Nested documents",
"Nested documents": "Documents imbriqués",
"Anyone with the link <1></1>can view this document": "Tous ceux avec le lien <1></1> peuvent voir ce document",
"Share": "Partager",
"Share this document": "Share this document",
"Share this document": "Partager ce document",
"This document is shared because the parent <em>{{ documentTitle }}</em> is publicly shared": "This document is shared because the parent <em>{{ documentTitle }}</em> is publicly shared",
"Publish to internet": "Publish to internet",
"Publish to internet": "Publier sur Internet",
"Anyone with the link can view this document": "Anyone with the link can view this document",
"Only team members with permission can view": "Only team members with permission can view",
"The shared link was last accessed {{ timeAgo }}.": "The shared link was last accessed {{ timeAgo }}.",
@@ -308,6 +313,9 @@
"Deleting": "Suppression",
"Im sure  Delete": "Je suis sûr Supprimer",
"Archiving": "Archiving",
"Couldnt create the document, try again?": "Couldnt create the document, try again?",
"Document permanently deleted": "Document supprimé définitivement",
"Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.": "Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.",
"Search documents": "Rechercher des documents",
"No documents found for your filters.": "Aucun documents trouvés pour votre recherche.",
"Youve not got any drafts at the moment.": "Vous n'avez aucun brouillon pour le moment.",
@@ -333,19 +341,19 @@
"Toggle navigation": "Toggle navigation",
"Focus search input": "Focus search input",
"Open this guide": "Ouvrir ce guide",
"Save document and exit": "Save document and exit",
"Publish document and exit": "Publish document and exit",
"Save document": "Save document",
"Save document and exit": "Enregistrer le document et quitter",
"Publish document and exit": "Publier le document et quitter",
"Save document": "Enregistrer le document",
"Cancel editing": "Annuler l'édition",
"Formatting": "Formatting",
"Paragraph": "Paragraph",
"Formatting": "Mise en forme",
"Paragraph": "Paragraphe",
"Large header": "En-tête large",
"Medium header": "En-tête moyen",
"Small header": "Petit en-tête",
"Underline": "Souligner",
"Undo": "Annuler",
"Redo": "Rétablir",
"Lists": "Lists",
"Lists": "Listes",
"Indent list item": "Indent list item",
"Outdent list item": "Outdent list item",
"Move list item up": "Move list item up",
@@ -365,44 +373,44 @@
"All documents": "All documents",
"Include documents that are in the archive": "Include documents that are in the archive",
"Any author": "Any author",
"Author": "Author",
"Author": "Auteur",
"Not Found": "Non trouvé",
"We were unable to find the page youre looking for.": "Nous n'avons pas pu trouver la page que vous recherchez.",
"Use the <em>{{ meta }}+K</em> shortcut to search from anywhere in your knowledge base": "Utilisez le <em>{{ meta }}+ K</em> raccourci pour rechercher n'importe où dans votre base de connaissances",
"No documents found for your search filters. <1></1>": "No documents found for your search filters. <1></1>",
"Create a new document?": "Create a new document?",
"Create a new document?": "Créer un nouveau document?",
"Clear filters": "Effacer les filtres",
"Email": "Email",
"Last active": "Last active",
"Role": "Role",
"Viewer": "Viewer",
"Email": "E-mail",
"Last active": "Dernière activité",
"Role": "Rôle",
"Viewer": "Lecteur",
"Suspended": "Suspendu",
"Shared": "Shared",
"by {{ name }}": "by {{ name }}",
"Shared": "Partagé",
"by {{ name }}": "par {{ name }}",
"Last accessed": "Last accessed",
"Add to Slack": "Add to Slack",
"Add to Slack": "Ajouter à Slack",
"Active": "Actif",
"Everyone": "Everyone",
"Admins": "Admins",
"Everyone": "Tout le monde",
"Admins": "Administrateurs",
"New group": "Nouveau Groupe",
"Groups can be used to organize and manage the people on your team.": "Groups can be used to organize and manage the people on your team.",
"New group": "New group",
"All groups": "All groups",
"All groups": "Tous les groupes",
"No groups have been created yet": "No groups have been created yet",
"Import started": "Import started",
"Export in progress…": "Export in progress…",
"Export in progress…": "Export en cours…",
"It is possible to import a zip file of folders and Markdown files previously exported from an Outline instance. Support will soon be added for importing from other services.": "It is possible to import a zip file of folders and Markdown files previously exported from an Outline instance. Support will soon be added for importing from other services.",
"Your file has been uploaded and the import is currently being processed, you can safely leave this page while it completes.": "Your file has been uploaded and the import is currently being processed, you can safely leave this page while it completes.",
"Your file has been uploaded and the import is currently being processed, you can safely leave this page while it completes.": "Votre fichier a été téléchargé et l'importation est en cours de traitement, vous pouvez quitter cette page en toute sécurité.",
"Sorry, the file <em>{{ fileName }}</em> is missing valid collections or documents.": "Sorry, the file <em>{{ fileName }}</em> is missing valid collections or documents.",
"<em>{{ fileName }}</em> looks good, the following collections and their documents will be imported:": "<em>{{ fileName }}</em> looks good, the following collections and their documents will be imported:",
"Uploading": "Uploading",
"Uploading": "Transfert en cours",
"Confirm & Import": "Confirm & Import",
"Choose File": "Choose File",
"Choose File": "Choisissez fichier",
"A full export might take some time, consider exporting a single document or collection if possible. Well put together a zip of all your documents in Markdown format and email it to <em>{{ userEmail }}</em>.": "A full export might take some time, consider exporting a single document or collection if possible. Well put together a zip of all your documents in Markdown format and email it to <em>{{ userEmail }}</em>.",
"Export Requested": "Export Requested",
"Requesting Export": "Requesting Export",
"Export Data": "Exporter les données",
"Everyone that has signed into Outline appears here. Its possible that there are other users who have access through {team.signinMethods} but havent signed in yet.": "Everyone that has signed into Outline appears here. Its possible that there are other users who have access through {team.signinMethods} but havent signed in yet.",
"Filter": "Filter",
"Filter": "Filtre",
"Profile saved": "Profil enregistré",
"Profile picture updated": "Photo de profil sauvegardée",
"Unable to upload new profile picture": "Impossible d'envoyer la nouvelle photo de profil",
@@ -417,18 +425,22 @@
"Documents that have been shared are listed below. Anyone that has the public link can access a read-only version of the document until the link has been revoked.": "Documents that have been shared are listed below. Anyone that has the public link can access a read-only version of the document until the link has been revoked.",
"Sharing is currently disabled.": "Sharing is currently disabled.",
"You can globally enable and disable public document sharing in the <em>security settings</em>.": "You can globally enable and disable public document sharing in the <em>security settings</em>.",
"Shared documents": "Shared documents",
"No share links, yet.": "No share links, yet.",
"Shared documents": "Documents partagés",
"No share links, yet.": "Aucun lien de partage pour le moment.",
"Whoops, you need to accept the permissions in Slack to connect Outline to your team. Try again?": "Whoops, you need to accept the permissions in Slack to connect Outline to your team. Try again?",
"Something went wrong while authenticating your request. Please try logging in again?": "Something went wrong while authenticating your request. Please try logging in again?",
"Get rich previews of Outline links shared in Slack and use the <em>{{ command }}</em> slash command to search for documents without leaving your chat.": "Get rich previews of Outline links shared in Slack and use the <em>{{ command }}</em> slash command to search for documents without leaving your chat.",
"Disconnect": "Disconnect",
"Disconnect": "Se déconnecter",
"Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.": "Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.",
"Connected to the <em>{{ channelName }}</em> channel": "Connected to the <em>{{ channelName }}</em> channel",
"Connect": "Connect",
"Connect": "Se connecter",
"New token": "Nouveau jeton",
"You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.": "You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.",
"Tokens": "Jetons",
"Create a token": "Créer un jeton",
"Youve not starred any documents yet.": "Vous n'avez encore ajouté aucun document aux favoris.",
"There are no templates just yet.": "There are no templates just yet.",
"You can create templates to help your team create consistent and accurate documentation.": "You can create templates to help your team create consistent and accurate documentation.",
"There are no templates just yet.": "Il n'y a pas encore de modèles.",
"You can create templates to help your team create consistent and accurate documentation.": "Il n'y a pas encore de modèles. Vous pouvez créer des modèles pour aider votre équipe à créer une documentation cohérente et précise.",
"Trash is empty at the moment.": "La corbeille est vide pour le moment.",
"You joined": "Vous avez rejoint",
"Joined": "Rejoint",
+13 -1
View File
@@ -109,6 +109,7 @@
"Dismiss": "Chiudi",
"Keyboard shortcuts": "Scorciatoie tastiera",
"Back": "Back",
"Collections could not be loaded, please reload the app": "Collections could not be loaded, please reload the app",
"New collection": "Nuova raccolta",
"Collections": "Raccolta",
"Untitled": "Senza titolo",
@@ -180,12 +181,14 @@
"Create template": "Crea un modello",
"Duplicate": "Duplica",
"Unpublish": "Depubblica",
"Permanently delete": "Permanently delete",
"Move": "Sposta",
"History": "Cronologia",
"Download": "Download",
"Print": "Stampa",
"Move {{ documentName }}": "Move {{ documentName }}",
"Delete {{ documentName }}": "Elimina {{ documentName }}",
"Permanently delete {{ documentName }}": "Permanently delete {{ documentName }}",
"Edit group": "Modifica gruppo",
"Delete group": "Elimina gruppo",
"Group options": "Opzioni del gruppo",
@@ -215,6 +218,8 @@
"Revoke invite": "Revoca invito",
"Activate account": "Attiva account",
"Suspend account": "Sospendi account",
"API token created": "API token created",
"Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".": "Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".",
"Documents": "Documenti",
"The document archive is empty at the moment.": "L'archivio documenti è vuoto al momento.",
"Search in collection": "Cerca nella raccolta",
@@ -308,6 +313,9 @@
"Deleting": "Sto eliminando",
"Im sure  Delete": "Sono sicuro Elimina",
"Archiving": "Archiviazione in corso",
"Couldnt create the document, try again?": "Couldnt create the document, try again?",
"Document permanently deleted": "Document permanently deleted",
"Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.": "Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.",
"Search documents": "Cerca documenti",
"No documents found for your filters.": "Nessun documento trovato per i tuoi filtri.",
"Youve not got any drafts at the moment.": "Al momento non hai bozze.",
@@ -384,8 +392,8 @@
"Active": "Active",
"Everyone": "Everyone",
"Admins": "Admins",
"Groups can be used to organize and manage the people on your team.": "Groups can be used to organize and manage the people on your team.",
"New group": "New group",
"Groups can be used to organize and manage the people on your team.": "Groups can be used to organize and manage the people on your team.",
"All groups": "All groups",
"No groups have been created yet": "No groups have been created yet",
"Import started": "Import started",
@@ -426,6 +434,10 @@
"Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.": "Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.",
"Connected to the <em>{{ channelName }}</em> channel": "Connected to the <em>{{ channelName }}</em> channel",
"Connect": "Connect",
"New token": "New token",
"You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.": "You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.",
"Tokens": "Tokens",
"Create a token": "Create a token",
"Youve not starred any documents yet.": "Non hai ancora contrassegnato alcun documento.",
"There are no templates just yet.": "There are no templates just yet.",
"You can create templates to help your team create consistent and accurate documentation.": "You can create templates to help your team create consistent and accurate documentation.",
+13 -1
View File
@@ -109,6 +109,7 @@
"Dismiss": "終了",
"Keyboard shortcuts": "キーボードショートカット",
"Back": "戻る",
"Collections could not be loaded, please reload the app": "Collections could not be loaded, please reload the app",
"New collection": "コレクションを追加",
"Collections": "コレクション",
"Untitled": "文書",
@@ -180,12 +181,14 @@
"Create template": "テンプレートの作成",
"Duplicate": "文書の複製",
"Unpublish": "未発表",
"Permanently delete": "Permanently delete",
"Move": "移動",
"History": "変更履歴",
"Download": "ダウンロード",
"Print": "プリント",
"Move {{ documentName }}": "{{ documentName }} を移動",
"Delete {{ documentName }}": "{{ documentName }}を削除します。",
"Permanently delete {{ documentName }}": "Permanently delete {{ documentName }}",
"Edit group": "グループの編集",
"Delete group": "グループを削除",
"Group options": "グループのオプション",
@@ -215,6 +218,8 @@
"Revoke invite": "招待を取り消す",
"Activate account": "アカウントの有効化",
"Suspend account": "アカウントの一時停止",
"API token created": "APIトークンが作成されました",
"Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".": "Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".",
"Documents": "ドキュメント",
"The document archive is empty at the moment.": "ドキュメントアーカイブは現在空です。",
"Search in collection": "コレクション内を検索",
@@ -308,6 +313,9 @@
"Deleting": "削除中",
"Im sure  Delete": "間違いありません – 削除",
"Archiving": "アーカイブ中",
"Couldnt create the document, try again?": "Couldnt create the document, try again?",
"Document permanently deleted": "Document permanently deleted",
"Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.": "Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.",
"Search documents": "ドキュメントの検索",
"No documents found for your filters.": "検索結果はありませんでした。",
"Youve not got any drafts at the moment.": "この時は下書きがない。",
@@ -384,8 +392,8 @@
"Active": "アクティブ",
"Everyone": "すべてのユーザー",
"Admins": "管理者",
"Groups can be used to organize and manage the people on your team.": "グループを使用して、チームのメンバーを整理および管理できます。",
"New group": "新規グループ",
"Groups can be used to organize and manage the people on your team.": "グループを使用して、チームのメンバーを整理および管理できます。",
"All groups": "すべてのグループ",
"No groups have been created yet": "グループはまだ作成されていません",
"Import started": "インポートを開始しました",
@@ -426,6 +434,10 @@
"Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.": "Outline コレクションを Slack チャネルに接続すると、ドキュメントの公開または更新時に、メッセージが自動的に Slack に投稿されます。",
"Connected to the <em>{{ channelName }}</em> channel": "<em>{{ channelName }}</em> チャンネルに接続",
"Connect": "接続",
"New token": "新しいトークン",
"You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.": "APIトークンはいくつでも作成できます。トークンには、ユーザーアカウントと同じ権限があります。\n 詳細については、 <em>開発者向けドキュメント</em>参照してください。",
"Tokens": "トークン",
"Create a token": "トークンを作成する",
"Youve not starred any documents yet.": "あなたは文書に「スター」を付けていません。",
"There are no templates just yet.": "テンプレートが見つかりません。",
"You can create templates to help your team create consistent and accurate documentation.": "チームが一貫性のある正確なドキュメントを作成するのに役立つテンプレートを作成できます。",
+13 -1
View File
@@ -109,6 +109,7 @@
"Dismiss": "닫기",
"Keyboard shortcuts": "단축키",
"Back": "뒤로 가기",
"Collections could not be loaded, please reload the app": "Collections could not be loaded, please reload the app",
"New collection": "새 컬렉션",
"Collections": "컬렉션",
"Untitled": "제목없음",
@@ -180,12 +181,14 @@
"Create template": "템플릿 만들기",
"Duplicate": "복사하기",
"Unpublish": "게시 취소",
"Permanently delete": "Permanently delete",
"Move": "이동",
"History": "히스토리",
"Download": "다운로드",
"Print": "인쇄",
"Move {{ documentName }}": "{{ documentName }} 를 이동함",
"Delete {{ documentName }}": "{{ documentName }} 삭제",
"Permanently delete {{ documentName }}": "Permanently delete {{ documentName }}",
"Edit group": "그룹 수정",
"Delete group": "그룹 삭제",
"Group options": "그룹 옵션",
@@ -215,6 +218,8 @@
"Revoke invite": "초대 취소",
"Activate account": "계정 활성화",
"Suspend account": "계정 정지",
"API token created": "API token created",
"Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".": "Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".",
"Documents": "문서",
"The document archive is empty at the moment.": "현재 보관 문서함이 비어 있습니다.",
"Search in collection": "컬렉션 검색",
@@ -308,6 +313,9 @@
"Deleting": "삭제 중",
"Im sure  Delete": "Im sure  Delete",
"Archiving": "보관",
"Couldnt create the document, try again?": "Couldnt create the document, try again?",
"Document permanently deleted": "Document permanently deleted",
"Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.": "Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.",
"Search documents": "문서 검색",
"No documents found for your filters.": "검색조건과 맞는 문서를 찾을 수 없습니다.",
"Youve not got any drafts at the moment.": "현재 임시보관 된 문서가 없습니다.",
@@ -384,8 +392,8 @@
"Active": "활성",
"Everyone": "모두",
"Admins": "관리자",
"Groups can be used to organize and manage the people on your team.": "Groups can be used to organize and manage the people on your team.",
"New group": "그룹 만들기",
"Groups can be used to organize and manage the people on your team.": "Groups can be used to organize and manage the people on your team.",
"All groups": "모든 그룹",
"No groups have been created yet": "아직 생성 된 그룹이 없습니다",
"Import started": "가져오기 시작됨",
@@ -426,6 +434,10 @@
"Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.": "Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.",
"Connected to the <em>{{ channelName }}</em> channel": "Connected to the <em>{{ channelName }}</em> channel",
"Connect": "Connect",
"New token": "New token",
"You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.": "You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.",
"Tokens": "Tokens",
"Create a token": "Create a token",
"Youve not starred any documents yet.": "중요 표시된 문서가 없어요.",
"There are no templates just yet.": "There are no templates just yet.",
"You can create templates to help your team create consistent and accurate documentation.": "You can create templates to help your team create consistent and accurate documentation.",
+13 -1
View File
@@ -109,6 +109,7 @@
"Dismiss": "Negeer",
"Keyboard shortcuts": "Keyboard shortcuts",
"Back": "Terug",
"Collections could not be loaded, please reload the app": "Collections could not be loaded, please reload the app",
"New collection": "Nieuwe collectie",
"Collections": "Collecties",
"Untitled": "Naamloos",
@@ -180,12 +181,14 @@
"Create template": "Create template",
"Duplicate": "Dupliceer",
"Unpublish": "Unpublish",
"Permanently delete": "Permanently delete",
"Move": "Verplaats",
"History": "Geschiedenis",
"Download": "Download",
"Print": "Print",
"Move {{ documentName }}": "Verplaats {{ documentName }}",
"Delete {{ documentName }}": "Delete {{ documentName }}",
"Permanently delete {{ documentName }}": "Permanently delete {{ documentName }}",
"Edit group": "Wijzig groep",
"Delete group": "Verwijder groep",
"Group options": "Groepsopties",
@@ -215,6 +218,8 @@
"Revoke invite": "Revoke invite",
"Activate account": "Activeer account",
"Suspend account": "Suspend account",
"API token created": "API token created",
"Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".": "Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".",
"Documents": "Documenten",
"The document archive is empty at the moment.": "The document archive is empty at the moment.",
"Search in collection": "Zoek in collecties",
@@ -308,6 +313,9 @@
"Deleting": "Deleting",
"Im sure  Delete": "Im sure  Delete",
"Archiving": "Archiving",
"Couldnt create the document, try again?": "Couldnt create the document, try again?",
"Document permanently deleted": "Document permanently deleted",
"Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.": "Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.",
"Search documents": "Search documents",
"No documents found for your filters.": "No documents found for your filters.",
"Youve not got any drafts at the moment.": "Youve not got any drafts at the moment.",
@@ -384,8 +392,8 @@
"Active": "Actief",
"Everyone": "Iedereen",
"Admins": "Admins",
"Groups can be used to organize and manage the people on your team.": "Groups can be used to organize and manage the people on your team.",
"New group": "New group",
"Groups can be used to organize and manage the people on your team.": "Groups can be used to organize and manage the people on your team.",
"All groups": "Alle groepen",
"No groups have been created yet": "No groups have been created yet",
"Import started": "Importeren gestart",
@@ -426,6 +434,10 @@
"Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.": "Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.",
"Connected to the <em>{{ channelName }}</em> channel": "Connected to the <em>{{ channelName }}</em> channel",
"Connect": "Connect",
"New token": "New token",
"You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.": "You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.",
"Tokens": "Tokens",
"Create a token": "Create a token",
"Youve not starred any documents yet.": "Je hebt nog geen favoriete documenten.",
"There are no templates just yet.": "Er zijn nog geen sjablonen.",
"You can create templates to help your team create consistent and accurate documentation.": "You can create templates to help your team create consistent and accurate documentation.",
+148 -136
View File
@@ -1,19 +1,19 @@
{
"currently editing": "aktualnie edytujesz",
"currently viewing": "obecnie wyświetlasz",
"previously edited": "previously edited",
"You": "You",
"previously edited": "poprzednio edytowane",
"You": "Ty",
"Viewers": "Oglądający",
"Sorry, an error occurred saving the collection": "Sorry, an error occurred saving the collection",
"Sorry, an error occurred saving the collection": "Przepraszamy, wystąpił błąd podczas zapisywania kolekcji",
"Add a description": "Dodaj opis",
"Collapse": "Collapse",
"Expand": "Expand",
"Submenu": "Submenu",
"Trash": "Trash",
"Archive": "Archive",
"Collapse": "Zwiń",
"Expand": "Rozwiń",
"Submenu": "Podmenu",
"Trash": "Kosz",
"Archive": "Archiwum",
"Drafts": "Kopie robocze",
"Templates": "Templates",
"Deleted Collection": "Deleted Collection",
"Templates": "Szablony",
"Deleted Collection": "Usunięta kolekcja",
"New": "New",
"Only visible to you": "Widoczne tylko dla Ciebie",
"Draft": "Szkic",
@@ -25,15 +25,15 @@
"published": "published",
"saved": "saved",
"updated": "updated",
"Never viewed": "Never viewed",
"Viewed": "Viewed",
"in": "in",
"nested document": "nested document",
"nested document_plural": "nested documents",
"Never viewed": "Nigdy nie oglądane",
"Viewed": "Przejrzane",
"in": "w",
"nested document": "zagnieżdżony dokument",
"nested document_plural": "zagnieżdżone dokumenty",
"Viewed by": "Wyświetlone przez",
"only you": "only you",
"person": "person",
"people": "people",
"only you": "tylko ty",
"person": "osoba",
"people": "osoby",
"Currently editing": "Currently editing",
"Currently viewing": "Currently viewing",
"Viewed {{ timeAgo }} ago": "Viewed {{ timeAgo }} ago",
@@ -56,32 +56,32 @@
"Delete row": "Usuń wiersz",
"Delete table": "Usuń tabelę",
"Delete image": "Usuń obraz",
"Download image": "Download image",
"Download image": "Pobierz obraz",
"Float left": "Float left",
"Float right": "Float right",
"Center large": "Center large",
"Italic": "Kursywa",
"Sorry, that link wont work for this embed type": "Ten link nie zadziała dla tego typu osadzenia",
"Find or create a doc": "Find or create a doc",
"Big heading": "Big heading",
"Medium heading": "Medium heading",
"Small heading": "Small heading",
"Heading": "Heading",
"Divider": "Divider",
"Find or create a doc": "Znajdź lub utwórz dokument",
"Big heading": "Duży nagłówek",
"Medium heading": "Średni nagłówek",
"Small heading": "Mały nagłówek",
"Heading": "Nagłówek",
"Divider": "Separator",
"Image": "Image",
"Sorry, an error occurred uploading the image": "Sorry, an error occurred uploading the image",
"Sorry, an error occurred uploading the image": "Przepraszamy, wystąpił błąd podczas przesyłania obrazka",
"Info": "Info",
"Info notice": "Info notice",
"Link": "Link",
"Link copied to clipboard": "Link copied to clipboard",
"Highlight": "Podkreślenie",
"Type '/' to insert": "Type '/' to insert",
"Type '/' to insert": "Wpisz '/', aby wstawić",
"Keep typing to filter": "Keep typing to filter",
"No results": "Brak wyników",
"Open link": "Otwórz link",
"Ordered list": "Ordered list",
"Page break": "Page break",
"Paste a link": "Paste a link",
"Paste a link": "Wklej link",
"Paste a {{service}} link…": "Paste a {{service}} link…",
"Placeholder": "Placeholder",
"Quote": "Cytat",
@@ -90,14 +90,14 @@
"Strikethrough": "Strikethrough",
"Bold": "Bold",
"Subheading": "Subheading",
"Table": "Table",
"Table": "Tabela",
"Tip": "Tip",
"Tip notice": "Tip notice",
"Warning": "Warning",
"Warning notice": "Warning notice",
"Icon": "Icon",
"Show menu": "Show menu",
"Choose icon": "Choose icon",
"Show menu": "Pokaż menu",
"Choose icon": "Wybierz ikonę",
"Loading": "Loading",
"Search": "Szukaj",
"Default access": "Domyślny dostęp",
@@ -109,55 +109,56 @@
"Dismiss": "Dismiss",
"Keyboard shortcuts": "Keyboard shortcuts",
"Back": "Wstecz",
"Collections could not be loaded, please reload the app": "Collections could not be loaded, please reload the app",
"New collection": "New collection",
"Collections": "Kolekcje",
"Untitled": "Untitled",
"Document not supported try Markdown, Plain text, HTML, or Word": "Document not supported try Markdown, Plain text, HTML, or Word",
"Untitled": "Bez tytułu",
"Document not supported try Markdown, Plain text, HTML, or Word": "Dokument nie jest obsługiwany wypróbuj Markdown, zwykły tekst, HTML lub Word",
"Home": "Home",
"Starred": "Oznaczone gwiazdką",
"Settings": "Settings",
"Invite people": "Invite people",
"Settings": "Ustawienia",
"Invite people": "Zaproś ludzi",
"Create a collection": "Utwórz kolekcję",
"Return to App": "Back to App",
"Account": "Account",
"Profile": "Profile",
"Notifications": "Notifications",
"Return to App": "Wróć do Aplikacji",
"Account": "Konto",
"Profile": "Profil",
"Notifications": "Powiadomienia",
"API Tokens": "Tokeny API",
"Team": "Zespół",
"Details": "Details",
"Security": "Security",
"Members": "Members",
"Groups": "Groups",
"Share Links": "Share Links",
"Import": "Import",
"Export": "Export",
"Integrations": "Integrations",
"Installation": "Installation",
"Details": "Szczegóły",
"Security": "Bezpieczeństwo",
"Members": "Członkowie",
"Groups": "Grupy",
"Share Links": "Udostępnione linki",
"Import": "Importuj",
"Export": "Eksportuj",
"Integrations": "Integracje",
"Installation": "Instalacja",
"Unstar": "Unstar",
"Star": "Star",
"Previous page": "Previous page",
"Next page": "Next page",
"Could not import file": "Could not import file",
"Appearance": "Appearance",
"Previous page": "Poprzednia strona",
"Next page": "Następna strona",
"Could not import file": "Nie można zaimportować pliku",
"Appearance": "Wygląd",
"System": "System",
"Light": "Light",
"Dark": "Dark",
"Light": "Jasny",
"Dark": "Ciemny",
"API documentation": "Dokumentacja API",
"Changelog": "Changelog",
"Send us feedback": "Send us feedback",
"Report a bug": "Report a bug",
"Log out": "Log out",
"Show path to document": "Show path to document",
"Path to document": "Path to document",
"Changelog": "Lista zmian",
"Send us feedback": "Prześlij nam swoją opinię",
"Report a bug": "Zgłoś błąd",
"Log out": "Wyloguj",
"Show path to document": "Pokaż ścieżkę do dokumentu",
"Path to document": "Ścieżka do dokumentu",
"Group member options": "Group member options",
"Remove": "Remove",
"Collection": "Collection",
"Remove": "Usuń",
"Collection": "Kolekcja",
"New document": "Nowy dokument",
"Import document": "Import document",
"Edit": "Edit",
"Permissions": "Permissions",
"Delete": "Delete",
"Collection permissions": "Collection permissions",
"Import document": "Zaimportuj dokument",
"Edit": "Edytuj",
"Permissions": "Uprawnienia",
"Delete": "Usuń",
"Collection permissions": "Uprawnienia kolekcji",
"Edit collection": "Edit collection",
"Delete collection": "Delete collection",
"Export collection": "Export collection",
@@ -165,11 +166,11 @@
"Sort in sidebar": "Sort in sidebar",
"Alphabetical sort": "Alphabetical sort",
"Manual sort": "Manual sort",
"Document duplicated": "Document duplicated",
"Document archived": "Document archived",
"Document restored": "Document restored",
"Document unpublished": "Document unpublished",
"Document options": "Document options",
"Document duplicated": "Dokument zduplikowany",
"Document archived": "Dokument zarchiwizowany",
"Document restored": "Dokument przywrócony",
"Document unpublished": "Dokument nieopublikowany",
"Document options": "Opcje dokumentu",
"Restore": "Restore",
"Choose a collection": "Choose a collection",
"Unpin": "Unpin",
@@ -178,33 +179,35 @@
"Disable embeds": "Disable embeds",
"New nested document": "Nowy zagnieżdżony dokument",
"Create template": "Create template",
"Duplicate": "Duplicate",
"Unpublish": "Unpublish",
"Move": "Move",
"History": "History",
"Download": "Download",
"Print": "Print",
"Move {{ documentName }}": "Move {{ documentName }}",
"Delete {{ documentName }}": "Delete {{ documentName }}",
"Edit group": "Edit group",
"Delete group": "Delete group",
"Group options": "Group options",
"Member options": "Member options",
"collection": "collection",
"Duplicate": "Zduplikuj",
"Unpublish": "Cofnij publikację",
"Permanently delete": "Permanently delete",
"Move": "Przenieś",
"History": "Historia",
"Download": "Pobierz",
"Print": "Drukuj",
"Move {{ documentName }}": "Przenieś {{ documentName }}",
"Delete {{ documentName }}": "Usuń {{ documentName }}",
"Permanently delete {{ documentName }}": "Permanently delete {{ documentName }}",
"Edit group": "Edytuj grupę",
"Delete group": "Usuń grupę",
"Group options": "Opcje grupy",
"Member options": "Opcje użytkownika",
"collection": "kolekcja",
"New child document": "Nowy dokument podrzędny",
"New document in <em>{{ collectionName }}</em>": "Nowy dokument w <em>{{ collectionName }}</em>",
"New template": "New template",
"Link copied": "Link copied",
"Revision options": "Revision options",
"Restore version": "Restore version",
"Copy link": "Copy link",
"Share link revoked": "Share link revoked",
"Share link copied": "Share link copied",
"Share options": "Share options",
"Go to document": "Go to document",
"Revoke link": "Revoke link",
"New template": "Nowy szablon",
"Link copied": "Link skopiowany",
"Revision options": "Opcje rewizji",
"Restore version": "Przywróć wersję",
"Copy link": "Kopiuj link",
"Share link revoked": "Unieważniono link udostępniania",
"Share link copied": "Skopiowano link udostępnienia",
"Share options": "Opcje udostępniania",
"Go to document": "Przejdź do dokumentu",
"Revoke link": "Unieważnij link",
"By {{ author }}": "By {{ author }}",
"Are you sure you want to make {{ userName }} an admin? Admins can modify team and billing information.": "Are you sure you want to make {{ userName }} an admin? Admins can modify team and billing information.",
"Are you sure you want to make {{ userName }} an admin? Admins can modify team and billing information.": "Czy na pewno chcesz uczynić {{ userName }} administratorem? Administratorzy mogą modyfikować informacje o zespole i rozliczeniach.",
"Are you sure you want to make {{ userName }} a member?": "Are you sure you want to make {{ userName }} a member?",
"Are you sure you want to make {{ userName }} a read-only viewer? They will not be able to edit any content": "Are you sure you want to make {{ userName }} a read-only viewer? They will not be able to edit any content",
"Are you sure you want to suspend this account? Suspended users will be prevented from logging in.": "Are you sure you want to suspend this account? Suspended users will be prevented from logging in.",
@@ -215,12 +218,14 @@
"Revoke invite": "Revoke invite",
"Activate account": "Activate account",
"Suspend account": "Suspend account",
"Documents": "Documents",
"The document archive is empty at the moment.": "The document archive is empty at the moment.",
"API token created": "API token created",
"Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".": "Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".",
"Documents": "Dokumenty",
"The document archive is empty at the moment.": "Archiwum dokumentu jest w tej chwili puste.",
"Search in collection": "Search in collection",
"<em>{{ collectionName }}</em> doesnt contain any\n documents yet.": "<em>{{ collectionName }}</em> doesnt contain any\n documents yet.",
"Get started by creating a new one!": "Get started by creating a new one!",
"Create a document": "Create a document",
"Create a document": "Utwórz dokument",
"Manage permissions": "Manage permissions",
"This collection is only visible to those given access": "This collection is only visible to those given access",
"Private": "Private",
@@ -229,11 +234,11 @@
"Recently published": "Recently published",
"Least recently updated": "Least recently updated",
"AZ": "AZ",
"Drop documents to import": "Drop documents to import",
"Drop documents to import": "Upuść dokumenty do zaimportowania",
"The collection was updated": "The collection was updated",
"You can edit the name and other details at any time, however doing so often might confuse your team mates.": "You can edit the name and other details at any time, however doing so often might confuse your team mates.",
"Name": "Name",
"Alphabetical": "Alphabetical",
"Alphabetical": "Alfabetycznie",
"Public document sharing": "Public document sharing",
"When enabled, documents can be shared publicly on the internet.": "When enabled, documents can be shared publicly on the internet.",
"Public sharing is currently disabled in the team security settings.": "Public sharing is currently disabled in the team security settings.",
@@ -251,7 +256,7 @@
"Search groups": "Search groups",
"No groups matching your search": "No groups matching your search",
"No groups left to add": "No groups left to add",
"Add": "Add",
"Add": "Dodaj",
"{{ userName }} was added to the collection": "{{ userName }} was added to the collection",
"Need to add someone whos not yet on the team yet?": "Need to add someone whos not yet on the team yet?",
"Invite people to {{ teamName }}": "Invite people to {{ teamName }}",
@@ -262,7 +267,7 @@
"Active <1></1> ago": "Active <1></1> ago",
"Never signed in": "Never signed in",
"Invited": "Invited",
"Admin": "Admin",
"Admin": "Administrator",
"{{ userName }} was removed from the collection": "{{ userName }} was removed from the collection",
"Could not remove user": "Could not remove user",
"{{ userName }} permissions were updated": "{{ userName }} permissions were updated",
@@ -308,6 +313,9 @@
"Deleting": "Deleting",
"Im sure  Delete": "Im sure  Delete",
"Archiving": "Archiving",
"Couldnt create the document, try again?": "Couldnt create the document, try again?",
"Document permanently deleted": "Document permanently deleted",
"Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.": "Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.",
"Search documents": "Search documents",
"No documents found for your filters.": "No documents found for your filters.",
"Youve not got any drafts at the moment.": "Youve not got any drafts at the moment.",
@@ -325,27 +333,27 @@
"Recently viewed": "Recently viewed",
"Created by me": "Created by me",
"Navigation": "Navigation",
"Edit current document": "Edit current document",
"Move current document": "Move current document",
"Edit current document": "Edytuj bieżący dokument",
"Move current document": "Przenieś bieżący dokument",
"Jump to search": "Jump to search",
"Jump to home": "Jump to home",
"Table of contents": "Table of contents",
"Toggle navigation": "Toggle navigation",
"Focus search input": "Focus search input",
"Open this guide": "Open this guide",
"Save document and exit": "Save document and exit",
"Publish document and exit": "Publish document and exit",
"Save document": "Save document",
"Save document and exit": "Zapisz dokument i wyjdź",
"Publish document and exit": "Opublikuj dokument i wyjdź",
"Save document": "Zapisz dokument",
"Cancel editing": "Cancel editing",
"Formatting": "Formatting",
"Paragraph": "Paragraph",
"Large header": "Large header",
"Medium header": "Medium header",
"Small header": "Small header",
"Formatting": "Formatowanie",
"Paragraph": "Akapit",
"Large header": "Duży nagłówek",
"Medium header": "Średni nagłówek",
"Small header": "Mały nagłówek",
"Underline": "Underline",
"Undo": "Undo",
"Redo": "Redo",
"Lists": "Lists",
"Undo": "Cofnij",
"Redo": "Ponów",
"Lists": "Listy",
"Indent list item": "Indent list item",
"Outdent list item": "Outdent list item",
"Move list item up": "Move list item up",
@@ -360,12 +368,12 @@
"Past week": "Past week",
"Past month": "Past month",
"Past year": "Past year",
"Active documents": "Active documents",
"Active documents": "Aktywne dokumenty",
"Documents in collections you are able to access": "Documents in collections you are able to access",
"All documents": "All documents",
"All documents": "Wszystkie dokumenty",
"Include documents that are in the archive": "Include documents that are in the archive",
"Any author": "Any author",
"Author": "Author",
"Author": "Autor",
"Not Found": "Not Found",
"We were unable to find the page youre looking for.": "We were unable to find the page youre looking for.",
"Use the <em>{{ meta }}+K</em> shortcut to search from anywhere in your knowledge base": "Use the <em>{{ meta }}+K</em> shortcut to search from anywhere in your knowledge base",
@@ -378,14 +386,14 @@
"Viewer": "Viewer",
"Suspended": "Suspended",
"Shared": "Shared",
"by {{ name }}": "by {{ name }}",
"by {{ name }}": "od {{ name }}",
"Last accessed": "Last accessed",
"Add to Slack": "Add to Slack",
"Active": "Active",
"Everyone": "Everyone",
"Admins": "Admins",
"Groups can be used to organize and manage the people on your team.": "Groups can be used to organize and manage the people on your team.",
"New group": "New group",
"Groups can be used to organize and manage the people on your team.": "Groups can be used to organize and manage the people on your team.",
"All groups": "All groups",
"No groups have been created yet": "No groups have been created yet",
"Import started": "Import started",
@@ -406,33 +414,37 @@
"Profile saved": "Profile saved",
"Profile picture updated": "Profile picture updated",
"Unable to upload new profile picture": "Unable to upload new profile picture",
"Photo": "Photo",
"Upload": "Upload",
"Photo": "Zdjęcie",
"Upload": "Prześlij",
"Full name": "Full name",
"Language": "Language",
"Language": "Język",
"Please note that translations are currently in early access.<1></1>Community contributions are accepted though our <4>translation portal</4>": "Tłumaczenia są w wczesnej fazie rozwoju i są <4>tworzone oraz sprawdzane przez społeczność</4>",
"Delete Account": "Delete Account",
"Delete Account": "Usuń konto",
"You may delete your account at any time, note that this is unrecoverable": "You may delete your account at any time, note that this is unrecoverable",
"Delete account": "Delete account",
"Delete account": "Usuń konto",
"Documents that have been shared are listed below. Anyone that has the public link can access a read-only version of the document until the link has been revoked.": "Documents that have been shared are listed below. Anyone that has the public link can access a read-only version of the document until the link has been revoked.",
"Sharing is currently disabled.": "Sharing is currently disabled.",
"You can globally enable and disable public document sharing in the <em>security settings</em>.": "You can globally enable and disable public document sharing in the <em>security settings</em>.",
"Shared documents": "Shared documents",
"No share links, yet.": "No share links, yet.",
"Shared documents": "Udostępnione dokumenty",
"No share links, yet.": "Nie ma jeszcze udostępnionych linków.",
"Whoops, you need to accept the permissions in Slack to connect Outline to your team. Try again?": "Whoops, you need to accept the permissions in Slack to connect Outline to your team. Try again?",
"Something went wrong while authenticating your request. Please try logging in again?": "Something went wrong while authenticating your request. Please try logging in again?",
"Get rich previews of Outline links shared in Slack and use the <em>{{ command }}</em> slash command to search for documents without leaving your chat.": "Get rich previews of Outline links shared in Slack and use the <em>{{ command }}</em> slash command to search for documents without leaving your chat.",
"Disconnect": "Disconnect",
"Disconnect": "Rozłącz",
"Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.": "Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.",
"Connected to the <em>{{ channelName }}</em> channel": "Connected to the <em>{{ channelName }}</em> channel",
"Connect": "Connect",
"Connect": "Połącz",
"New token": "New token",
"You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.": "You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.",
"Tokens": "Tokens",
"Create a token": "Create a token",
"Youve not starred any documents yet.": "Youve not starred any documents yet.",
"There are no templates just yet.": "There are no templates just yet.",
"There are no templates just yet.": "Nie ma jeszcze żadnych szablonów.",
"You can create templates to help your team create consistent and accurate documentation.": "Możesz utworzyć szablony, aby ułatwić Twoim członkom zespołu tworzenie spójnych i dokładnych dokumentacji.",
"Trash is empty at the moment.": "Trash is empty at the moment.",
"You joined": "You joined",
"Joined": "Joined",
"{{ time }} ago.": "{{ time }} ago.",
"Trash is empty at the moment.": "Kosz jest w tej chwili pusty.",
"You joined": "Dołączyłeś",
"Joined": "Dołączył",
"{{ time }} ago.": "{{ time }} temu.",
"Edit Profile": "Edit Profile",
"{{ userName }} hasnt updated any documents yet.": "{{ userName }} hasnt updated any documents yet."
"{{ userName }} hasnt updated any documents yet.": "{{ userName }} nie zaktualizował jeszcze żadnych dokumentów."
}
+13 -1
View File
@@ -109,6 +109,7 @@
"Dismiss": "Descartar",
"Keyboard shortcuts": "Atalhos de teclado",
"Back": "Voltar",
"Collections could not be loaded, please reload the app": "Não foi possível carregar as coleções, recarregue o aplicativo",
"New collection": "Nova coleção",
"Collections": "Coleções",
"Untitled": "Sem título",
@@ -180,12 +181,14 @@
"Create template": "Criar Template",
"Duplicate": "Duplicar",
"Unpublish": "Cancelar publicação",
"Permanently delete": "Apagar permanentemente",
"Move": "Mover",
"History": "Histórico",
"Download": "Fazer download",
"Print": "Imprimir",
"Move {{ documentName }}": "Mover {{ documentName }}",
"Delete {{ documentName }}": "Excluir {{ documentName }}",
"Permanently delete {{ documentName }}": "Apagar permanentemente {{ documentName }}",
"Edit group": "Editar grupo",
"Delete group": "Remover grupo",
"Group options": "Opções do grupo",
@@ -215,6 +218,8 @@
"Revoke invite": "Revogar convite",
"Activate account": "Ativar a conta",
"Suspend account": "Suspender conta",
"API token created": "Token de API criado",
"Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".": "Dê ao seu token um nome que o ajude a se lembrar de como ele será usado no futuro, por exemplo, \"desenvolvimento local\", \"produção\" ou \"integração contínua\".",
"Documents": "Documentos",
"The document archive is empty at the moment.": "O arquivo do documento está vazio no momento.",
"Search in collection": "Pesquisa na coleção",
@@ -308,6 +313,9 @@
"Deleting": "Excluindo",
"Im sure  Delete": "Tenho certeza - Excluir  ",
"Archiving": "Arquivando",
"Couldnt create the document, try again?": "Não foi possível criar o documento, deseja tentar novamente?",
"Document permanently deleted": "Documento apagado permanentemente",
"Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.": "Tem certeza de que deseja apagar permanentemente o documento <em>{{ documentTitle }}</em> Esta ação é imediata e não pode ser desfeita.",
"Search documents": "Procurar documentos",
"No documents found for your filters.": "Nenhum documento foi encontrado para seus filtros.",
"Youve not got any drafts at the moment.": "Você não tem rascunhos no momento.",
@@ -384,8 +392,8 @@
"Active": "Ativo",
"Everyone": "Todos",
"Admins": "Administradores",
"Groups can be used to organize and manage the people on your team.": "Grupos podem ser usados para organizar e gerenciar as pessoas da sua equipe.",
"New group": "Novo grupo",
"Groups can be used to organize and manage the people on your team.": "Grupos podem ser usados para organizar e gerenciar as pessoas da sua equipe.",
"All groups": "Todos os grupos",
"No groups have been created yet": "Nenhum grupo foi criado ainda",
"Import started": "Importação iniciada",
@@ -426,6 +434,10 @@
"Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.": "Conecte as coleções do Outline a canais do Slack e mensagens serão automaticamente publicadas no Slack quando os documentos forem publicados ou atualizados.",
"Connected to the <em>{{ channelName }}</em> channel": "Conectado ao canal <em>{{ channelName }}</em>",
"Connect": "Conectar",
"New token": "Novo token",
"You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.": "Você pode criar uma quantidade ilimitada de tokens pessoais para autenticar\n com a API. Os tokens têm as mesmas permissões da sua conta de usuário.\n Para obter mais detalhes, consulte a <em>documentação do desenvolvedor</em>.",
"Tokens": "Tokens",
"Create a token": "Criar um token",
"Youve not starred any documents yet.": "Você ainda não marcou nenhum documento como favorito.",
"There are no templates just yet.": "Ainda não há modelos.",
"You can create templates to help your team create consistent and accurate documentation.": "Você pode criar modelos para ajudar sua equipe a criar documentação consistente e precisa.",
+13 -1
View File
@@ -109,6 +109,7 @@
"Dismiss": "Dispensar",
"Keyboard shortcuts": "Atalhos do teclado",
"Back": "Back",
"Collections could not be loaded, please reload the app": "Collections could not be loaded, please reload the app",
"New collection": "Nova coleção",
"Collections": "Coleções",
"Untitled": "Sem título",
@@ -180,12 +181,14 @@
"Create template": "Criar template",
"Duplicate": "Duplicar",
"Unpublish": "Despublicar",
"Permanently delete": "Permanently delete",
"Move": "Mover",
"History": "Historia",
"Download": "Download",
"Print": "Imprimir",
"Move {{ documentName }}": "Move {{ documentName }}",
"Delete {{ documentName }}": "Apagar {{ documentName }}",
"Permanently delete {{ documentName }}": "Permanently delete {{ documentName }}",
"Edit group": "Editar grupo",
"Delete group": "Apagar grupo",
"Group options": "Group options",
@@ -215,6 +218,8 @@
"Revoke invite": "Revogar convite",
"Activate account": "Ativar conta",
"Suspend account": "Suspender conta",
"API token created": "API token created",
"Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".": "Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".",
"Documents": "Documentos",
"The document archive is empty at the moment.": "O arquivo do documento está vazio neste momento.",
"Search in collection": "Procurar na coleção",
@@ -308,6 +313,9 @@
"Deleting": "Excluindo",
"Im sure  Delete": "Tenho certeza - Excluir",
"Archiving": "Archiving",
"Couldnt create the document, try again?": "Couldnt create the document, try again?",
"Document permanently deleted": "Document permanently deleted",
"Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.": "Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.",
"Search documents": "Search documents",
"No documents found for your filters.": "Nenhum documento encontrado com os seus filtros.",
"Youve not got any drafts at the moment.": "Não tem nenhum rascunho de momento.",
@@ -384,8 +392,8 @@
"Active": "Active",
"Everyone": "Everyone",
"Admins": "Admins",
"Groups can be used to organize and manage the people on your team.": "Groups can be used to organize and manage the people on your team.",
"New group": "New group",
"Groups can be used to organize and manage the people on your team.": "Groups can be used to organize and manage the people on your team.",
"All groups": "All groups",
"No groups have been created yet": "No groups have been created yet",
"Import started": "Import started",
@@ -426,6 +434,10 @@
"Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.": "Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.",
"Connected to the <em>{{ channelName }}</em> channel": "Connected to the <em>{{ channelName }}</em> channel",
"Connect": "Connect",
"New token": "New token",
"You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.": "You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.",
"Tokens": "Tokens",
"Create a token": "Create a token",
"Youve not starred any documents yet.": "Ainda não marcou nenhum documento com estrela.",
"There are no templates just yet.": "There are no templates just yet.",
"You can create templates to help your team create consistent and accurate documentation.": "You can create templates to help your team create consistent and accurate documentation.",
+13 -1
View File
@@ -109,6 +109,7 @@
"Dismiss": "Убрать",
"Keyboard shortcuts": "Горячие клавиши",
"Back": "Назад",
"Collections could not be loaded, please reload the app": "Collections could not be loaded, please reload the app",
"New collection": "Новая коллекция",
"Collections": "Коллекции",
"Untitled": "Без названия",
@@ -180,12 +181,14 @@
"Create template": "Создать шаблон",
"Duplicate": "Создать копию",
"Unpublish": "Снять с публикации",
"Permanently delete": "Permanently delete",
"Move": "Переместить",
"History": "История",
"Download": "Скачать",
"Print": "Печать",
"Move {{ documentName }}": "Переместить {{ documentName }}",
"Delete {{ documentName }}": "Удалить {{ documentName }}",
"Permanently delete {{ documentName }}": "Permanently delete {{ documentName }}",
"Edit group": "Редактировать группу",
"Delete group": "Удалить группу",
"Group options": "Параметры группы",
@@ -215,6 +218,8 @@
"Revoke invite": "Отозвать приглашение",
"Activate account": "Включить аккаунт",
"Suspend account": "Заблокировать аккаунт",
"API token created": "API token created",
"Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".": "Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".",
"Documents": "Документы",
"The document archive is empty at the moment.": "Архив документов на данный момент пуст.",
"Search in collection": "Поиск в коллекции",
@@ -308,6 +313,9 @@
"Deleting": "Удаление",
"Im sure  Delete": "Я уверен – удалить",
"Archiving": "Архивирование",
"Couldnt create the document, try again?": "Couldnt create the document, try again?",
"Document permanently deleted": "Document permanently deleted",
"Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.": "Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.",
"Search documents": "Искать документы",
"No documents found for your filters.": "По запросу ничего не найдено.",
"Youve not got any drafts at the moment.": "У вас пока нет черновиков.",
@@ -384,8 +392,8 @@
"Active": "Активный",
"Everyone": "Все",
"Admins": "Администраторы",
"Groups can be used to organize and manage the people on your team.": "Groups can be used to organize and manage the people on your team.",
"New group": "Новая группа",
"Groups can be used to organize and manage the people on your team.": "Groups can be used to organize and manage the people on your team.",
"All groups": "Все группы",
"No groups have been created yet": "No groups have been created yet",
"Import started": "Импорт начат",
@@ -426,6 +434,10 @@
"Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.": "Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.",
"Connected to the <em>{{ channelName }}</em> channel": "Connected to the <em>{{ channelName }}</em> channel",
"Connect": "Подключить",
"New token": "New token",
"You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.": "You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.",
"Tokens": "Tokens",
"Create a token": "Create a token",
"Youve not starred any documents yet.": "Вы еще не добавили ни одного документа в избранное.",
"There are no templates just yet.": "There are no templates just yet.",
"You can create templates to help your team create consistent and accurate documentation.": "You can create templates to help your team create consistent and accurate documentation.",
+13 -1
View File
@@ -109,6 +109,7 @@
"Dismiss": "Dismiss",
"Keyboard shortcuts": "Keyboard shortcuts",
"Back": "Tillbaka",
"Collections could not be loaded, please reload the app": "Collections could not be loaded, please reload the app",
"New collection": "New collection",
"Collections": "Collections",
"Untitled": "Untitled",
@@ -180,12 +181,14 @@
"Create template": "Create template",
"Duplicate": "Duplicera",
"Unpublish": "Unpublish",
"Permanently delete": "Permanently delete",
"Move": "Flytta",
"History": "Historik",
"Download": "Ladda ned",
"Print": "Skriv ut",
"Move {{ documentName }}": "Move {{ documentName }}",
"Delete {{ documentName }}": "Delete {{ documentName }}",
"Permanently delete {{ documentName }}": "Permanently delete {{ documentName }}",
"Edit group": "Edit group",
"Delete group": "Delete group",
"Group options": "Group options",
@@ -215,6 +218,8 @@
"Revoke invite": "Revoke invite",
"Activate account": "Activate account",
"Suspend account": "Suspend account",
"API token created": "API token created",
"Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".": "Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".",
"Documents": "Documents",
"The document archive is empty at the moment.": "The document archive is empty at the moment.",
"Search in collection": "Search in collection",
@@ -308,6 +313,9 @@
"Deleting": "Deleting",
"Im sure  Delete": "Im sure  Delete",
"Archiving": "Archiving",
"Couldnt create the document, try again?": "Couldnt create the document, try again?",
"Document permanently deleted": "Document permanently deleted",
"Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.": "Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.",
"Search documents": "Search documents",
"No documents found for your filters.": "No documents found for your filters.",
"Youve not got any drafts at the moment.": "Youve not got any drafts at the moment.",
@@ -384,8 +392,8 @@
"Active": "Active",
"Everyone": "Everyone",
"Admins": "Admins",
"Groups can be used to organize and manage the people on your team.": "Groups can be used to organize and manage the people on your team.",
"New group": "New group",
"Groups can be used to organize and manage the people on your team.": "Groups can be used to organize and manage the people on your team.",
"All groups": "All groups",
"No groups have been created yet": "No groups have been created yet",
"Import started": "Import started",
@@ -426,6 +434,10 @@
"Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.": "Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.",
"Connected to the <em>{{ channelName }}</em> channel": "Connected to the <em>{{ channelName }}</em> channel",
"Connect": "Connect",
"New token": "New token",
"You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.": "You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.",
"Tokens": "Tokens",
"Create a token": "Create a token",
"Youve not starred any documents yet.": "Youve not starred any documents yet.",
"There are no templates just yet.": "There are no templates just yet.",
"You can create templates to help your team create consistent and accurate documentation.": "You can create templates to help your team create consistent and accurate documentation.",
+13 -1
View File
@@ -109,6 +109,7 @@
"Dismiss": "ปิด",
"Keyboard shortcuts": "ปุ่มลัดแป้นพิมพ์",
"Back": "Back",
"Collections could not be loaded, please reload the app": "Collections could not be loaded, please reload the app",
"New collection": "คอลเลกชันใหม่",
"Collections": "Collections",
"Untitled": "ไม่มีชื่อ",
@@ -180,12 +181,14 @@
"Create template": "Create template",
"Duplicate": "Duplicate",
"Unpublish": "Unpublish",
"Permanently delete": "Permanently delete",
"Move": "Move",
"History": "History",
"Download": "Download",
"Print": "Print",
"Move {{ documentName }}": "Move {{ documentName }}",
"Delete {{ documentName }}": "Delete {{ documentName }}",
"Permanently delete {{ documentName }}": "Permanently delete {{ documentName }}",
"Edit group": "Edit group",
"Delete group": "Delete group",
"Group options": "Group options",
@@ -215,6 +218,8 @@
"Revoke invite": "Revoke invite",
"Activate account": "Activate account",
"Suspend account": "Suspend account",
"API token created": "API token created",
"Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".": "Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".",
"Documents": "Documents",
"The document archive is empty at the moment.": "The document archive is empty at the moment.",
"Search in collection": "Search in collection",
@@ -308,6 +313,9 @@
"Deleting": "Deleting",
"Im sure  Delete": "Im sure  Delete",
"Archiving": "Archiving",
"Couldnt create the document, try again?": "Couldnt create the document, try again?",
"Document permanently deleted": "Document permanently deleted",
"Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.": "Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.",
"Search documents": "Search documents",
"No documents found for your filters.": "No documents found for your filters.",
"Youve not got any drafts at the moment.": "Youve not got any drafts at the moment.",
@@ -384,8 +392,8 @@
"Active": "Active",
"Everyone": "Everyone",
"Admins": "Admins",
"Groups can be used to organize and manage the people on your team.": "Groups can be used to organize and manage the people on your team.",
"New group": "New group",
"Groups can be used to organize and manage the people on your team.": "Groups can be used to organize and manage the people on your team.",
"All groups": "All groups",
"No groups have been created yet": "No groups have been created yet",
"Import started": "Import started",
@@ -426,6 +434,10 @@
"Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.": "Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.",
"Connected to the <em>{{ channelName }}</em> channel": "Connected to the <em>{{ channelName }}</em> channel",
"Connect": "Connect",
"New token": "New token",
"You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.": "You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.",
"Tokens": "Tokens",
"Create a token": "Create a token",
"Youve not starred any documents yet.": "Youve not starred any documents yet.",
"There are no templates just yet.": "There are no templates just yet.",
"You can create templates to help your team create consistent and accurate documentation.": "You can create templates to help your team create consistent and accurate documentation.",
+14 -2
View File
@@ -109,6 +109,7 @@
"Dismiss": "忽略",
"Keyboard shortcuts": "快捷键",
"Back": "返回",
"Collections could not be loaded, please reload the app": "Collections could not be loaded, please reload the app",
"New collection": "新建文档集",
"Collections": "文档集",
"Untitled": "无标题",
@@ -180,12 +181,14 @@
"Create template": "创建模板",
"Duplicate": "复制",
"Unpublish": "取消发布",
"Permanently delete": "Permanently delete",
"Move": "移动",
"History": "历史记录",
"Download": "下载",
"Print": "打印",
"Move {{ documentName }}": "移动 {{ documentName }}",
"Delete {{ documentName }}": "删除 {{ documentName }}",
"Permanently delete {{ documentName }}": "Permanently delete {{ documentName }}",
"Edit group": "编辑群组",
"Delete group": "刪除群組",
"Group options": "分组选项",
@@ -215,6 +218,8 @@
"Revoke invite": "撤消邀请",
"Activate account": "激活帐号",
"Suspend account": "冻结账号",
"API token created": "API token created",
"Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".": "Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".",
"Documents": "文档",
"The document archive is empty at the moment.": "尚未归档的文档",
"Search in collection": "在文档集中搜索",
@@ -308,6 +313,9 @@
"Deleting": "正在删除",
"Im sure  Delete": "确定– 删除",
"Archiving": "正在归档",
"Couldnt create the document, try again?": "Couldnt create the document, try again?",
"Document permanently deleted": "Document permanently deleted",
"Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.": "Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.",
"Search documents": "搜索文档",
"No documents found for your filters.": "没有找到相关文档。",
"Youve not got any drafts at the moment.": "您目前还没有任何草稿。",
@@ -384,8 +392,8 @@
"Active": "活动的",
"Everyone": "所有人",
"Admins": "管理員",
"Groups can be used to organize and manage the people on your team.": "Groups can be used to organize and manage the people on your team.",
"New group": "New group",
"Groups can be used to organize and manage the people on your team.": "Groups can be used to organize and manage the people on your team.",
"All groups": "All groups",
"No groups have been created yet": "No groups have been created yet",
"Import started": "已开始导入",
@@ -414,7 +422,7 @@
"Delete Account": "删除帐户",
"You may delete your account at any time, note that this is unrecoverable": "您可以随时删除您的帐户,请注意删除后将无法恢复该账号",
"Delete account": "删除账户",
"Documents that have been shared are listed below. Anyone that has the public link can access a read-only version of the document until the link has been revoked.": "Documents that have been shared are listed below. Anyone that has the public link can access a read-only version of the document until the link has been revoked.",
"Documents that have been shared are listed below. Anyone that has the public link can access a read-only version of the document until the link has been revoked.": "这是您分享给其他人的所有文档,在您撤销链接之前,任何人都可以通过该链接阅读该文档",
"Sharing is currently disabled.": "Sharing is currently disabled.",
"You can globally enable and disable public document sharing in the <em>security settings</em>.": "You can globally enable and disable public document sharing in the <em>security settings</em>.",
"Shared documents": "Shared documents",
@@ -426,6 +434,10 @@
"Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.": "Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.",
"Connected to the <em>{{ channelName }}</em> channel": "Connected to the <em>{{ channelName }}</em> channel",
"Connect": "Connect",
"New token": "New token",
"You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.": "You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.",
"Tokens": "Tokens",
"Create a token": "Create a token",
"Youve not starred any documents yet.": "您尚未标记任何文档。",
"There are no templates just yet.": "There are no templates just yet.",
"You can create templates to help your team create consistent and accurate documentation.": "You can create templates to help your team create consistent and accurate documentation.",
+450
View File
@@ -0,0 +1,450 @@
{
"currently editing": "正在編輯",
"currently viewing": "正在瀏覽",
"previously edited": "先前編輯過",
"You": "你",
"Viewers": "瀏覽者",
"Sorry, an error occurred saving the collection": "抱歉,儲存到文件集時發生了錯誤。",
"Add a description": "新增描述",
"Collapse": "收合",
"Expand": "展開",
"Submenu": "子選單",
"Trash": "垃圾桶",
"Archive": "封存",
"Drafts": "草稿",
"Templates": "文件範本",
"Deleted Collection": "刪除文件集",
"New": "新",
"Only visible to you": "只有您可以檢視",
"Draft": "草稿",
"Template": "範本",
"New doc": "新文件",
"deleted": "已刪除",
"archived": "已封存",
"created": "已建立",
"published": "已發佈",
"saved": "已儲存",
"updated": "已更新",
"Never viewed": "從未瀏覽過",
"Viewed": "已瀏覽",
"in": "在",
"nested document": "子文件",
"nested document_plural": "子文件",
"Viewed by": "已瀏覽過",
"only you": "僅限自己",
"person": "person",
"people": "people",
"Currently editing": "正在編輯",
"Currently viewing": "正在瀏覽",
"Viewed {{ timeAgo }} ago": "{{ timeAgo }} 前瀏覽過",
"Insert column after": "插入欄位於右方",
"Insert column before": "插入欄位於左方",
"Insert row after": "插入下方列",
"Insert row before": "插入上方列",
"Align center": "置中對齊",
"Align left": "靠左對齊",
"Align right": "靠右對齊",
"Bulleted list": "項目清單",
"Todo list": "待辦清單",
"Code block": "程式碼區塊",
"Copied to clipboard": "已複製到剪貼簿",
"Code": "程式碼",
"Create link": "建立超連結",
"Sorry, an error occurred creating the link": "抱歉,建立分享連結時發生錯誤",
"Create a new doc": "建立新文件",
"Delete column": "刪除欄",
"Delete row": "刪除列",
"Delete table": "刪除表格",
"Delete image": "刪除圖片",
"Download image": "下載圖片",
"Float left": "Float left",
"Float right": "Float right",
"Center large": "Center large",
"Italic": "斜體",
"Sorry, that link wont work for this embed type": "抱歉,不支援將這個連結嵌入到文件",
"Find or create a doc": "尋找或建立文件",
"Big heading": "大標題",
"Medium heading": "中標題",
"Small heading": "小標題",
"Heading": "標題",
"Divider": "分隔線",
"Image": "圖片",
"Sorry, an error occurred uploading the image": "抱歉,上傳圖片時發生錯誤",
"Info": "Info",
"Info notice": "Info notice",
"Link": "連結",
"Link copied to clipboard": "已經將連結複製到剪貼簿",
"Highlight": "強調",
"Type '/' to insert": "輸入 '/' 進行插入",
"Keep typing to filter": "繼續輸入以進行篩選",
"No results": "找不到任何結果",
"Open link": "開啟連結",
"Ordered list": "有序清單",
"Page break": "分頁符號",
"Paste a link": "貼上連結",
"Paste a {{service}} link…": "貼上 {{service}} 連結...",
"Placeholder": "占位符",
"Quote": "引用",
"Remove link": "移除超連結",
"Search or paste a link": "搜尋或貼上連結",
"Strikethrough": "刪除線",
"Bold": "粗體",
"Subheading": "副標題",
"Table": "表格",
"Tip": "提示",
"Tip notice": "Tip notice",
"Warning": "警告",
"Warning notice": "Warning notice",
"Icon": "圖示",
"Show menu": "顯示選單",
"Choose icon": "選擇圖示",
"Loading": "正在讀取",
"Search": "搜尋",
"Default access": "預設存取權限",
"View and edit": "檢視與編輯",
"View only": "唯讀",
"No access": "無存取權限",
"Outline is available in your language {{optionLabel}}, would you like to change?": "Outline 現在已經可以使用您的語言 {{optionLabel}},要現在切換嗎?",
"Change Language": "變更語言",
"Dismiss": "Dismiss",
"Keyboard shortcuts": "鍵盤快捷鍵",
"Back": "上一步",
"Collections could not be loaded, please reload the app": "文件集無法被載入,請重新整理 App",
"New collection": "新文件集",
"Collections": "文件集",
"Untitled": "無標題",
"Document not supported try Markdown, Plain text, HTML, or Word": "不支援的文件格式-請嘗試使用 Markdown、純文字、HTML 或 Word 格式",
"Home": "首頁",
"Starred": "我的最愛",
"Settings": "設定",
"Invite people": "邀請使用者",
"Create a collection": "建立一個文件集",
"Return to App": "回到 App",
"Account": "帳號",
"Profile": "個人資料",
"Notifications": "通知",
"API Tokens": "API 權杖",
"Team": "團隊",
"Details": "詳細",
"Security": "安全性",
"Members": "成員",
"Groups": "群組",
"Share Links": "分享連結",
"Import": "匯入",
"Export": "匯出",
"Integrations": "Integrations",
"Installation": "安裝",
"Unstar": "取消收藏",
"Star": "加入收藏",
"Previous page": "上一頁",
"Next page": "下一頁",
"Could not import file": "無法匯入",
"Appearance": "介面設定",
"System": "系統",
"Light": "明亮",
"Dark": "暗淡",
"API documentation": "API 文件",
"Changelog": "更新日誌",
"Send us feedback": "回饋意見給我們",
"Report a bug": "回報 Bug",
"Log out": "登出",
"Show path to document": "顯示文件路徑",
"Path to document": "文件路徑",
"Group member options": "群組成員選項",
"Remove": "移除",
"Collection": "文件集",
"New document": "建立新文件",
"Import document": "匯入文件",
"Edit": "編輯",
"Permissions": "權限設定",
"Delete": "刪除",
"Collection permissions": "文件集權限",
"Edit collection": "編輯文件集",
"Delete collection": "刪除文件集",
"Export collection": "匯出文件集",
"Show sort menu": "顯示排序選單",
"Sort in sidebar": "在側邊欄中排序",
"Alphabetical sort": "依照字母排序",
"Manual sort": "手動排序",
"Document duplicated": "文件已複製",
"Document archived": "文件已被封存",
"Document restored": "文件已經被還原",
"Document unpublished": "文件已經回復為草稿狀態",
"Document options": "文件選項",
"Restore": "還原",
"Choose a collection": "選擇一個文件集",
"Unpin": "取消釘選",
"Pin to collection": "釘選在文件集",
"Enable embeds": "顯示嵌入物件",
"Disable embeds": "不顯示嵌入物件",
"New nested document": "建立新的子文件",
"Create template": "建立範本",
"Duplicate": "複製",
"Unpublish": "下架",
"Permanently delete": "永久刪除",
"Move": "移動",
"History": "歷史紀錄",
"Download": "下載",
"Print": "列印",
"Move {{ documentName }}": "移動 {{ documentName }}",
"Delete {{ documentName }}": "刪除 {{ documentName }}",
"Permanently delete {{ documentName }}": "永久刪除 {{ documentName }}",
"Edit group": "編輯群組",
"Delete group": "刪除群組",
"Group options": "群組選項",
"Member options": "成員選項",
"collection": "文件集",
"New child document": "建立新的子文件",
"New document in <em>{{ collectionName }}</em>": "新文件在<em>{{ collectionName }}</em>",
"New template": "新增範本",
"Link copied": "已複製連結",
"Revision options": "修訂版本選項",
"Restore version": "還原版本",
"Copy link": "複製連結",
"Share link revoked": "註銷分享連結",
"Share link copied": "分享連結已複製",
"Share options": "分享設定",
"Go to document": "跳轉到文件",
"Revoke link": "註銷連結",
"By {{ author }}": "由 {{ author }}",
"Are you sure you want to make {{ userName }} an admin? Admins can modify team and billing information.": "你確定要將 {{ userName }} 設為管理員嗎?管理員可以修改團隊及帳單資訊。",
"Are you sure you want to make {{ userName }} a member?": "您卻將要將 {{ userName }} 設定設為成員之一嗎?",
"Are you sure you want to make {{ userName }} a read-only viewer? They will not be able to edit any content": "你確定要將 {{ userName }} 設定為唯讀檢視者嗎?他們將無法編輯任何內容。",
"Are you sure you want to suspend this account? Suspended users will be prevented from logging in.": "你確定要停用這個使用者帳號嗎?遭到停用的使用者將無法登入。",
"User options": "使用者選項",
"Make {{ userName }} a member": "將 {{ userName }} 設為成員",
"Make {{ userName }} a viewer": "將 {{ userName }} 設為檢視者",
"Make {{ userName }} an admin…": "讓 {{ userName }} 成為管理員...",
"Revoke invite": "取消邀請",
"Activate account": "啟用帳號",
"Suspend account": "停用帳號",
"API token created": "以建立 API 權杖",
"Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".": "為您的 Token 權杖命名,有助於您記住將其用在何種用途,例如「本機開發」、「正式環境」或是「持續整合」。",
"Documents": "所有文件",
"The document archive is empty at the moment.": "目前沒有任何文件被封存。",
"Search in collection": "在文件集中搜尋",
"<em>{{ collectionName }}</em> doesnt contain any\n documents yet.": "<em>{{ collectionName }}</em> 中還沒有任何文件。",
"Get started by creating a new one!": "開始建立一個新文件吧!",
"Create a document": "建立新文件",
"Manage permissions": "管理權限設定",
"This collection is only visible to those given access": "只有被授與存取權限的人才能檢視這個文件集",
"Private": "非公開",
"Pinned": "已釘選",
"Recently updated": "最近更新",
"Recently published": "最近發布",
"Least recently updated": "很久以前更新",
"AZ": "A-Z",
"Drop documents to import": "拖曳文件以進行匯入",
"The collection was updated": "文件已被更新",
"You can edit the name and other details at any time, however doing so often might confuse your team mates.": "你可以在任何時間編輯姓名或其他詳細資料,然而經常這麼做可能會讓其他團隊成員感到困擾。",
"Name": "名稱",
"Alphabetical": "依照字母",
"Public document sharing": "公開分享文件",
"When enabled, documents can be shared publicly on the internet.": "當啟用時,文件將會被公開分享到網際網路",
"Public sharing is currently disabled in the team security settings.": "團隊的安全性設定已經停用公開分享功能。",
"Saving": "正在儲存",
"Save": "儲存",
"Collections are for grouping your documents. They work best when organized around a topic or internal team — Product or Engineering for example.": "文件集是用於將您的文件進行分組歸類。依照主題或內部團隊來組織文件時將能發揮最好的效果——例如透過產品或工程團隊將文件進行分類。",
"This is the default level of access given to team members, you can give specific users or groups more access once the collection is created.": "這是設定團隊成員預設的存取權限,您可以在文件集建立之後針對特定的使用者或使用者群組給予更多的權限。",
"Creating": "正在建立",
"Create": "建立",
"{{ groupName }} was added to the collection": "{{ groupName }} 已經從文件集中移除",
"Could not add user": "無法新增使用者",
"Cant find the group youre looking for?": "找不到您要找的群組?",
"Create a group": "建立新群組",
"Search by group name": "依照群組名稱搜尋",
"Search groups": "搜尋群組",
"No groups matching your search": "找不到符合搜尋條件的群組",
"No groups left to add": "No groups left to add",
"Add": "新增",
"{{ userName }} was added to the collection": "{{ userName }} 已經被新增到文件集",
"Need to add someone whos not yet on the team yet?": "Need to add someone whos not yet on the team yet?",
"Invite people to {{ teamName }}": "邀請他人加入 {{ teamName }}",
"Search by name": "依名稱搜尋",
"Search people": "搜尋使用者",
"No people matching your search": "找不到符合搜尋條件的使用者",
"No people left to add": "No people left to add",
"Active <1></1> ago": "最後活動於 <1></1> 前",
"Never signed in": "從未登入",
"Invited": "已邀請",
"Admin": "管理員",
"{{ userName }} was removed from the collection": "{{ userName }} 已經從文件集中移除",
"Could not remove user": "無法移除使用者",
"{{ userName }} permissions were updated": "已經更新 {{ userName }} 的存取權限",
"Could not update user": "無法更新使用者資料",
"The {{ groupName }} group was removed from the collection": "群組 {{ groupName }} 已經從文件集中移除",
"Could not remove group": "無法移除群組",
"{{ groupName }} permissions were updated": "已經更新 {{ groupName }} 的存取權限",
"Default access permissions were updated": "已經更新預設存取權限設定",
"Could not update permissions": "無法更新存取權限設定",
"The <em>{{ collectionName }}</em> collection is private. Team members have no access to it by default.": "<em>{{ collectionName }}</em>文件集將被設為非公開。預設情況下,團隊成員將無法存取。",
"Team members can view documents in the <em>{{ collectionName }}</em> collection by default.": "預設情況下,團隊成員可以檢視<em>{{ collectionName }}</em>中的文件。",
"Team members can view and edit documents in the <em>{{ collectionName }}</em> collection by\n default.": "預設情況下,團隊成員可以檢視與編輯<em>{{ collectionName }}</em>中的文件。",
"Additional access": "額外的存取權限",
"Add groups": "新增群組",
"Add people": "新增人員",
"Add specific access for individual groups and team members": "為個別群組和團隊成員增加特定的存取權限",
"Add groups to {{ collectionName }}": "將群組加入到 {{ collectionName }}",
"Add people to {{ collectionName }}": "將使用者加入到 {{ collectionName }}",
"Hide contents": "隱藏內容",
"Show contents": "顯示內容",
"Edit {{noun}}": "編輯 {{noun}}",
"Archived": "已封存",
"Save Draft": "儲存為草稿",
"Done Editing": "編輯完成",
"New from template": "從範本建立",
"Publish": "發佈",
"Publishing": "正在發佈",
"Nested documents": "子文件",
"Anyone with the link <1></1>can view this document": "任何人只要擁有此連結 <1></1> 都可以檢視這份文件",
"Share": "分享",
"Share this document": "分享這份文件",
"This document is shared because the parent <em>{{ documentTitle }}</em> is publicly shared": "這份文件已經被公開分享,因爲已由其上層文件 <em>{{ documentTitle }}</em> 公開分享。",
"Publish to internet": "公開發佈到網際網路",
"Anyone with the link can view this document": "任何人只要擁有連結都可以檢視這份文件",
"Only team members with permission can view": "只有具有權限的團隊成員才可以檢視",
"The shared link was last accessed {{ timeAgo }}.": "分享連結最後一次被存取是 {{ timeAgo }} 前",
"Share nested documents": "被嵌入的所有文件也一併分享",
"Nested documents are publicly available": "子文件將可以被公開存取",
"Nested documents are not shared": "子文件將不會被分享",
"Are you sure you want to delete the <em>{{ documentTitle }}</em> template?": "你確定要刪除 <em>{{ documentTitle }}</em> 範本嗎?",
"Are you sure about that? Deleting the <em>{{ documentTitle }}</em> document will delete all of its history and any nested documents.": "你確定要這麼做嗎?將文件 <em>{{ documentTitle }}</em> 刪除同時也會刪除所有它的歷史編輯紀錄以及任何其所屬的子文件。",
"If youd like the option of referencing or restoring the {{noun}} in the future, consider archiving it instead.": "如果您希望將來可以引用或還原 {{noun}},請考慮改將其封存。",
"Deleting": "正在刪除",
"Im sure  Delete": "我確定要- 刪除",
"Archiving": "封存",
"Couldnt create the document, try again?": "無法建立文件,請再試一次?",
"Document permanently deleted": "文件已經被永久刪除",
"Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.": "你確定要永久刪除文件 <em>{{ documentTitle }}</em> 嗎?這個動作會立即生效且無法被回復。",
"Search documents": "搜尋文件",
"No documents found for your filters.": "沒有符合搜尋條件的文件。",
"Youve not got any drafts at the moment.": "目前您還沒有任何草稿。",
"Not found": "Not found",
"We were unable to find the page youre looking for. Go to the <2>homepage</2>?": "我們無法找到您所尋找的頁面。要回到<2>首頁</2>嗎?",
"Offline": "離線",
"We were unable to load the document while offline.": "我們無法在離線狀態下讀取文件。",
"Your account has been suspended": "您的使用者帳號已經被停用",
"A team admin (<em>{{ suspendedContactEmail }}</em>) has suspended your account. To re-activate your account, please reach out to them directly.": "團隊管理員 (<em>{{ suspendedContactEmail }}</em>) 已經將您的使用者帳號停用。請直接聯絡團隊管理員以協助您重新啟用帳號。",
"{{userName}} was added to the group": "{{userName}} 已經被加入到群組",
"Add team members below to give them access to the group. Need to add someone whos not yet on the team yet?": "在下方新增團隊成員來授權他們可以存取群組。需要增加那些尚未加入團隊的人嗎?",
"Invite them to {{teamName}}": "邀請他們加入 {{teamName}}",
"{{userName}} was removed from the group": "{{userName}} 已經從群組中移除",
"This group has no members.": "這個群組沒有成員。",
"Recently viewed": "最近瀏覽",
"Created by me": "由我建立的",
"Navigation": "導覽",
"Edit current document": "編輯目前文件",
"Move current document": "移動目前文件",
"Jump to search": "跳轉到搜尋",
"Jump to home": "跳轉到首頁",
"Table of contents": "目錄",
"Toggle navigation": "切換導覽",
"Focus search input": "Focus search input",
"Open this guide": "Open this guide",
"Save document and exit": "儲存文件並退出",
"Publish document and exit": "發佈並退出",
"Save document": "儲存文件",
"Cancel editing": "取消編輯",
"Formatting": "Formatting",
"Paragraph": "段落",
"Large header": "大標題",
"Medium header": "中標題",
"Small header": "小標題",
"Underline": "底線",
"Undo": "復原",
"Redo": "重做",
"Lists": "清單",
"Indent list item": "增加清單縮排",
"Outdent list item": "減少清單縮排",
"Move list item up": "將清單項目往上移",
"Move list item down": "將清單項目往下移",
"Numbered list": "編號清單",
"Blockquote": "引用區塊",
"Horizontal divider": "水平分隔線",
"Inline code": "行內程式碼",
"Any collection": "任何文件集",
"Any time": "任何時間",
"Past day": "過去一天",
"Past week": "上週",
"Past month": "上個月",
"Past year": "去年",
"Active documents": "使用中的文件",
"Documents in collections you are able to access": "您可以存取文件集中所有的文件",
"All documents": "所有文件",
"Include documents that are in the archive": "包含已封存的文件",
"Any author": "任何作者",
"Author": "作者",
"Not Found": "找不到頁面",
"We were unable to find the page youre looking for.": "我們無法找到您所尋找的頁面。",
"Use the <em>{{ meta }}+K</em> shortcut to search from anywhere in your knowledge base": "使用快捷鍵 <em>{{ meta }}+K</em> 可以隨時隨地搜尋知識庫",
"No documents found for your search filters. <1></1>": "根據您的搜尋過濾條件找不到任何文件。<1></1>",
"Create a new document?": "建立一份新的文件?",
"Clear filters": "清除篩選條件",
"Email": "電子郵件",
"Last active": "最近一次活動",
"Role": "角色",
"Viewer": "檢視者",
"Suspended": "已停用帳號",
"Shared": "已分享",
"by {{ name }}": "由 {{ name }}",
"Last accessed": "最近使用",
"Add to Slack": "新增到 Slack",
"Active": "上線",
"Everyone": "所有人",
"Admins": "管理員",
"New group": "建立群組",
"Groups can be used to organize and manage the people on your team.": "群組可以用來組織與管理團隊中的人員。",
"All groups": "所有群組",
"No groups have been created yet": "尚未建立任何群組",
"Import started": "已經開始匯入",
"Export in progress…": "正在匯出",
"It is possible to import a zip file of folders and Markdown files previously exported from an Outline instance. Support will soon be added for importing from other services.": "可以匯入先前從 Outline 匯出含有資料夾和 Markdown 檔案的 zip 壓縮檔。我們會盡快支援從其他服務匯入 Outline 的功能。",
"Your file has been uploaded and the import is currently being processed, you can safely leave this page while it completes.": "您的檔案已經被上傳,目前正在處理匯入工作,您可以放心離開此頁面,匯入工作將於背景完成。",
"Sorry, the file <em>{{ fileName }}</em> is missing valid collections or documents.": "抱歉,檔案 <em>{{ fileName }}</em> 中找不到有效的文件集或文件。",
"<em>{{ fileName }}</em> looks good, the following collections and their documents will be imported:": "<em>{{ fileName }}</em> 是 Outline 支援的檔案,以下的文件集與其包含之文件將會被匯入:",
"Uploading": "正在上傳",
"Confirm & Import": "確認並匯入",
"Choose File": "選擇檔案",
"A full export might take some time, consider exporting a single document or collection if possible. Well put together a zip of all your documents in Markdown format and email it to <em>{{ userEmail }}</em>.": "匯出所有文件集將會需要一些時間,可以的話,請考慮匯出單一的文件或文件集。我們會將您擁有的所有文件轉換為 Markdown 格式,封裝為一個 zip 壓縮檔,並在匯出完成時會寄出電子郵件到 <em>{{ userEmail }}</em>。",
"Export Requested": "已請求匯出",
"Requesting Export": "請求匯出",
"Export Data": "匯出資料",
"Everyone that has signed into Outline appears here. Its possible that there are other users who have access through {team.signinMethods} but havent signed in yet.": "所有登入到 Outline 的人會出現在此。但也有可能有其他使用者能夠透過 {team.signinMethods} 登入存取 Outline 但還沒登入過 Outline。",
"Filter": "篩選器",
"Profile saved": "個人資料已儲存",
"Profile picture updated": "大頭貼已上傳",
"Unable to upload new profile picture": "無法上傳新的大頭貼",
"Photo": "照片",
"Upload": "上傳",
"Full name": "全名",
"Language": "語言",
"Please note that translations are currently in early access.<1></1>Community contributions are accepted though our <4>translation portal</4>": "請注意多國語言翻譯目前處於嚐鮮階段。<1></1>我們樂於接受來自社群協作的翻譯,如果您有興趣參與歡迎到我們的<4>翻譯平台入口網站</4>",
"Delete Account": "刪除帳號",
"You may delete your account at any time, note that this is unrecoverable": "您隨時可以刪除您的使用者帳號,但此為不可回復之動作",
"Delete account": "刪除帳號",
"Documents that have been shared are listed below. Anyone that has the public link can access a read-only version of the document until the link has been revoked.": "下列清單為已被分享之文件。任何擁有公開分享連結的人,在分享連結被註銷前,都可以存取文件的唯讀版本。",
"Sharing is currently disabled.": "已停用分享功能。",
"You can globally enable and disable public document sharing in the <em>security settings</em>.": "您可以透過<em>安全性設定</em>針對全域啟用或停用文件公開分享的功能。",
"Shared documents": "已分享的文件",
"No share links, yet.": "尚未有分享連結",
"Whoops, you need to accept the permissions in Slack to connect Outline to your team. Try again?": "哇嗚,您必須在 Slack 允許 Outline \b取得權限連結您的團隊。再試一次?",
"Something went wrong while authenticating your request. Please try logging in again?": "請求認證時發生一些錯誤,請嘗試重新登入?",
"Get rich previews of Outline links shared in Slack and use the <em>{{ command }}</em> slash command to search for documents without leaving your chat.": "使用 <em>{{ command }}</em> slash command 功能搜尋文件不需跳開聊天視窗,而且可以在 Slack 獲得更多細節的 Outline 文件預覽功能。",
"Disconnect": "解除綁定",
"Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.": "將 Outline 文件集連結到 Slack 頻道,若文件已經被更新或公開釋出,則會被自動張貼在 Slack 上。",
"Connected to the <em>{{ channelName }}</em> channel": "連結到 <em>{{ channelName }}</em> 頻道",
"Connect": "綁定",
"New token": "新權杖",
"You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.": "你可以無限制數量地建立個人權杖來使用 API,而權杖所具有的權限與您的使用者帳號相同。\n若要了解更多請參閱<em>開發者文件</em>",
"Tokens": "權杖",
"Create a token": "建立權杖",
"Youve not starred any documents yet.": "你還沒有收藏任何文件到我的最愛。",
"There are no templates just yet.": "目前還沒有任何文件範本。",
"You can create templates to help your team create consistent and accurate documentation.": "你可以建立文件範本來幫助團隊建立一致且準確的文件。",
"Trash is empty at the moment.": "垃圾桶目前是空的。",
"You joined": "您已經加入",
"Joined": "已加入",
"{{ time }} ago.": "{{ time }} 前",
"Edit Profile": "編輯個人檔案",
"{{ userName }} hasnt updated any documents yet.": "{{ userName }} 還沒更新任何文件。"
}
+1 -1
View File
@@ -137,7 +137,7 @@ export const light = {
placeholder: "#a2b2c3",
sidebarBackground: colors.warmGrey,
sidebarItemBackground: colors.black10,
sidebarItemBackground: "#d7e0ea",
sidebarText: "rgb(78, 92, 110)",
backdrop: "rgba(0, 0, 0, 0.2)",
shadow: "rgba(0, 0, 0, 0.2)",
+555 -265
View File
File diff suppressed because it is too large Load Diff