mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e7f2c4c587 | |||
| 2a4b1d3371 | |||
| 2f09ef1928 | |||
| f6d889f759 | |||
| a50471959b | |||
| d8ad2fc1a2 | |||
| 0c48227b57 | |||
| 72da0653cc | |||
| e613ec732b | |||
| 0be40609ed | |||
| ec8fde0a5f | |||
| 2c52a8cb8b | |||
| 1db31eed41 | |||
| 8ba8013c6a | |||
| 1521d4dbac | |||
| a1a4fd1baf | |||
| 31f4424018 | |||
| 1f5b83aaeb | |||
| 77db0c2e95 | |||
| 4cbae1cf7d | |||
| e985078b80 | |||
| 09b73401de | |||
| 42b384688d | |||
| 5bdee1204e | |||
| 9db72217af | |||
| 57a2524fbd | |||
| bd148f4790 | |||
| 28d32af613 | |||
| f2f550e1d2 | |||
| dad21b2186 | |||
| 5fb5f1e8b5 | |||
| 2d0690697c | |||
| 6b551749d4 | |||
| 52fc861bcf | |||
| c81c9a9d2d | |||
| 29c742a673 | |||
| dd249021e7 | |||
| 21d3b9c7e0 | |||
| 6665dfff28 | |||
| cdfe3a7fc3 | |||
| 401c91f90b | |||
| ed5320507d |
+94
-2
@@ -1,4 +1,12 @@
|
||||
version: 2
|
||||
version: 2.1
|
||||
|
||||
executors:
|
||||
docker-publisher:
|
||||
environment:
|
||||
IMAGE_NAME: outlinewiki/outline
|
||||
docker:
|
||||
- image: circleci/buildpack-deps:stretch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
working_directory: ~/outline
|
||||
@@ -40,4 +48,88 @@ jobs:
|
||||
command: yarn test
|
||||
- run:
|
||||
name: build-webpack
|
||||
command: yarn build:webpack
|
||||
command: yarn build:webpack
|
||||
build-image:
|
||||
executor: docker-publisher
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker:
|
||||
version: 20.10.6
|
||||
- run:
|
||||
name: Build Docker image
|
||||
command: docker build -t $IMAGE_NAME:latest .
|
||||
- run:
|
||||
name: Archive Docker image
|
||||
command: docker save -o image.tar $IMAGE_NAME
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- ./image.tar
|
||||
publish-latest:
|
||||
executor: docker-publisher
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: /tmp/workspace
|
||||
- setup_remote_docker:
|
||||
version: 20.10.6
|
||||
- run:
|
||||
name: Load archived Docker image
|
||||
command: docker load -i /tmp/workspace/image.tar
|
||||
- run:
|
||||
name: Publish Docker Image to Docker Hub
|
||||
command: |
|
||||
echo "$DOCKERHUB_PASS" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin
|
||||
IMAGE_TAG=${CIRCLE_TAG/v/''}
|
||||
docker tag $IMAGE_NAME:latest $IMAGE_NAME:$IMAGE_TAG
|
||||
docker push $IMAGE_NAME:latest
|
||||
docker push $IMAGE_NAME:$IMAGE_TAG
|
||||
publish-tag:
|
||||
executor: docker-publisher
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: /tmp/workspace
|
||||
- setup_remote_docker:
|
||||
version: 20.10.6
|
||||
- run:
|
||||
name: Load archived Docker image
|
||||
command: docker load -i /tmp/workspace/image.tar
|
||||
- run:
|
||||
name: Publish Docker Image to Docker Hub
|
||||
command: |
|
||||
echo "$DOCKERHUB_PASS" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin
|
||||
IMAGE_TAG=${CIRCLE_TAG/v/''}
|
||||
docker tag $IMAGE_NAME:latest $IMAGE_NAME:$IMAGE_TAG
|
||||
docker push $IMAGE_NAME:$IMAGE_TAG
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build-and-test:
|
||||
jobs:
|
||||
- build:
|
||||
filters:
|
||||
tags:
|
||||
ignore: /^v.*/
|
||||
build-docker:
|
||||
jobs:
|
||||
- build-image:
|
||||
filters:
|
||||
tags:
|
||||
only: /^v.*/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
- publish-latest:
|
||||
requires:
|
||||
- build-image
|
||||
filters:
|
||||
tags:
|
||||
only: /^v\d+\.\d+\.\d+$/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
- publish-tag:
|
||||
requires:
|
||||
- build-image
|
||||
filters:
|
||||
tags:
|
||||
only: /^v\d+\.\d+\.\d+-.*$/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
|
||||
+40
-13
@@ -1,23 +1,50 @@
|
||||
FROM node:14-alpine
|
||||
# syntax=docker/dockerfile:1.2
|
||||
ARG APP_PATH=/opt/outline
|
||||
FROM node:14-alpine AS deps-common
|
||||
|
||||
ENV APP_PATH /opt/outline
|
||||
RUN mkdir -p $APP_PATH
|
||||
ARG APP_PATH
|
||||
WORKDIR $APP_PATH
|
||||
COPY ./package.json ./yarn.lock ./
|
||||
|
||||
# ---
|
||||
FROM deps-common AS deps-dev
|
||||
RUN yarn install --no-optional --frozen-lockfile && \
|
||||
yarn cache clean
|
||||
|
||||
# ---
|
||||
FROM deps-common AS deps-prod
|
||||
RUN yarn install --production=true --frozen-lockfile && \
|
||||
yarn cache clean
|
||||
|
||||
# ---
|
||||
FROM node:14-alpine AS builder
|
||||
|
||||
ARG APP_PATH
|
||||
WORKDIR $APP_PATH
|
||||
|
||||
COPY package.json ./
|
||||
COPY yarn.lock ./
|
||||
|
||||
RUN yarn --pure-lockfile
|
||||
|
||||
COPY . .
|
||||
COPY --from=deps-dev $APP_PATH/node_modules ./node_modules
|
||||
RUN yarn build
|
||||
|
||||
RUN yarn build && \
|
||||
yarn --production --ignore-scripts --prefer-offline && \
|
||||
rm -rf shared && \
|
||||
rm -rf app
|
||||
# ---
|
||||
FROM node:14-alpine AS runner
|
||||
|
||||
ARG APP_PATH
|
||||
WORKDIR $APP_PATH
|
||||
ENV NODE_ENV production
|
||||
CMD yarn start
|
||||
|
||||
COPY --from=builder $APP_PATH/build ./build
|
||||
COPY --from=builder $APP_PATH/server ./server
|
||||
COPY --from=builder $APP_PATH/public ./public
|
||||
COPY --from=builder $APP_PATH/.sequelizerc ./.sequelizerc
|
||||
COPY --from=deps-prod $APP_PATH/node_modules ./node_modules
|
||||
COPY --from=builder $APP_PATH/package.json ./package.json
|
||||
|
||||
RUN addgroup -g 1001 -S nodejs && \
|
||||
adduser -S nodejs -u 1001 && \
|
||||
chown -R nodejs:nodejs $APP_PATH/build
|
||||
|
||||
USER nodejs
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["yarn", "start"]
|
||||
|
||||
@@ -66,7 +66,11 @@ const RealButton = styled.button`
|
||||
|
||||
|
||||
&:hover {
|
||||
background: ${darken(0.05, props.theme.buttonNeutralBackground)};
|
||||
background: ${
|
||||
props.borderOnHover
|
||||
? props.theme.buttonNeutralBackground
|
||||
: darken(0.05, props.theme.buttonNeutralBackground)
|
||||
};
|
||||
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px, ${
|
||||
props.theme.buttonNeutralBorder
|
||||
} 0 0 0 1px inset;
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as React from "react";
|
||||
import { MenuItem as BaseMenuItem } from "reakit/Menu";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import MenuIconWrapper from "../MenuIconWrapper";
|
||||
|
||||
type Props = {|
|
||||
onClick?: (SyntheticEvent<>) => void | Promise<void>,
|
||||
@@ -16,6 +17,7 @@ type Props = {|
|
||||
as?: string | React.ComponentType<*>,
|
||||
hide?: () => void,
|
||||
level?: number,
|
||||
icon?: React.Node,
|
||||
|};
|
||||
|
||||
const MenuItem = ({
|
||||
@@ -25,6 +27,7 @@ const MenuItem = ({
|
||||
disabled,
|
||||
as,
|
||||
hide,
|
||||
icon,
|
||||
...rest
|
||||
}: Props) => {
|
||||
const handleClick = React.useCallback(
|
||||
@@ -71,6 +74,7 @@ const MenuItem = ({
|
||||
|
||||
</>
|
||||
)}
|
||||
{icon && <MenuIconWrapper>{icon}</MenuIconWrapper>}
|
||||
{children}
|
||||
</MenuAnchor>
|
||||
)}
|
||||
@@ -130,8 +134,8 @@ export const MenuAnchor = styled.a`
|
||||
`};
|
||||
|
||||
${breakpoint("tablet")`
|
||||
padding: ${(props) => (props.$toggleable ? "4px 12px" : "6px 12px")};
|
||||
font-size: 15px;
|
||||
padding: 4px 12px;
|
||||
font-size: 14px;
|
||||
`};
|
||||
`;
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
MenuItem as BaseMenuItem,
|
||||
} from "reakit/Menu";
|
||||
import styled from "styled-components";
|
||||
import Flex from "components/Flex";
|
||||
import MenuIconWrapper from "components/MenuIconWrapper";
|
||||
import Header from "./Header";
|
||||
import MenuItem, { MenuAnchor } from "./MenuItem";
|
||||
import Separator from "./Separator";
|
||||
@@ -67,7 +69,15 @@ export function filterTemplateItems(items: TMenuItem[]): TMenuItem[] {
|
||||
}
|
||||
|
||||
function Template({ items, ...menu }: Props): React.Node {
|
||||
return filterTemplateItems(items).map((item, index) => {
|
||||
const filteredTemplates = filterTemplateItems(items);
|
||||
const iconIsPresentInAnyMenuItem = filteredTemplates.find(
|
||||
(item) => !!item.icon
|
||||
);
|
||||
|
||||
return filteredTemplates.map((item, index) => {
|
||||
if (iconIsPresentInAnyMenuItem)
|
||||
item.icon = item.icon ? item.icon : <MenuIconWrapper />;
|
||||
|
||||
if (item.to) {
|
||||
return (
|
||||
<MenuItem
|
||||
@@ -76,6 +86,7 @@ function Template({ items, ...menu }: Props): React.Node {
|
||||
key={index}
|
||||
disabled={item.disabled}
|
||||
selected={item.selected}
|
||||
icon={item.icon}
|
||||
{...menu}
|
||||
>
|
||||
{item.title}
|
||||
@@ -92,6 +103,7 @@ function Template({ items, ...menu }: Props): React.Node {
|
||||
selected={item.selected}
|
||||
level={item.level}
|
||||
target={item.href.startsWith("#") ? undefined : "_blank"}
|
||||
icon={item.icon}
|
||||
{...menu}
|
||||
>
|
||||
{item.title}
|
||||
@@ -107,6 +119,7 @@ function Template({ items, ...menu }: Props): React.Node {
|
||||
disabled={item.disabled}
|
||||
selected={item.selected}
|
||||
key={index}
|
||||
icon={item.icon}
|
||||
{...menu}
|
||||
>
|
||||
{item.title}
|
||||
@@ -120,7 +133,7 @@ function Template({ items, ...menu }: Props): React.Node {
|
||||
key={index}
|
||||
as={Submenu}
|
||||
templateItems={item.items}
|
||||
title={item.title}
|
||||
title={<Title title={item.title} icon={item.icon} />}
|
||||
{...menu}
|
||||
/>
|
||||
);
|
||||
@@ -139,4 +152,13 @@ function Template({ items, ...menu }: Props): React.Node {
|
||||
});
|
||||
}
|
||||
|
||||
function Title({ title, icon }) {
|
||||
return (
|
||||
<Flex align="center">
|
||||
{icon && <MenuIconWrapper>{icon}</MenuIconWrapper>}
|
||||
{title}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo<Props>(Template);
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { CloseIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory, useRouteMatch } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import Event from "models/Event";
|
||||
import Button from "components/Button";
|
||||
import Empty from "components/Empty";
|
||||
import Flex from "components/Flex";
|
||||
import PaginatedEventList from "components/PaginatedEventList";
|
||||
import Scrollable from "components/Scrollable";
|
||||
import useStores from "hooks/useStores";
|
||||
import { documentUrl } from "utils/routeHelpers";
|
||||
|
||||
const EMPTY_ARRAY = [];
|
||||
|
||||
function DocumentHistory() {
|
||||
const { events, documents } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const match = useRouteMatch();
|
||||
const history = useHistory();
|
||||
|
||||
const document = documents.getByUrl(match.params.documentSlug);
|
||||
const eventsInDocument = document
|
||||
? events.inDocument(document.id)
|
||||
: EMPTY_ARRAY;
|
||||
|
||||
const onCloseHistory = () => {
|
||||
history.push(documentUrl(document));
|
||||
};
|
||||
|
||||
const items = React.useMemo(() => {
|
||||
if (
|
||||
eventsInDocument[0] &&
|
||||
document &&
|
||||
eventsInDocument[0].createdAt !== document.updatedAt
|
||||
) {
|
||||
eventsInDocument.unshift(
|
||||
new Event({
|
||||
name: "documents.latest_version",
|
||||
documentId: document.id,
|
||||
createdAt: document.updatedAt,
|
||||
actor: document.updatedBy,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return eventsInDocument;
|
||||
}, [eventsInDocument, document]);
|
||||
|
||||
return (
|
||||
<Sidebar>
|
||||
{document ? (
|
||||
<Position column>
|
||||
<Header>
|
||||
<Title>{t("History")}</Title>
|
||||
<Button
|
||||
icon={<CloseIcon />}
|
||||
onClick={onCloseHistory}
|
||||
borderOnHover
|
||||
neutral
|
||||
/>
|
||||
</Header>
|
||||
<Scrollable topShadow>
|
||||
<PaginatedEventList
|
||||
fetch={events.fetchPage}
|
||||
events={items}
|
||||
options={{ documentId: document.id }}
|
||||
document={document}
|
||||
empty={<Empty>{t("Oh weird, there's nothing here")}</Empty>}
|
||||
/>
|
||||
</Scrollable>
|
||||
</Position>
|
||||
) : null}
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
|
||||
const Position = styled(Flex)`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: ${(props) => props.theme.sidebarWidth}px;
|
||||
`;
|
||||
|
||||
const Sidebar = styled(Flex)`
|
||||
display: none;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
background: ${(props) => props.theme.background};
|
||||
width: ${(props) => props.theme.sidebarWidth}px;
|
||||
border-left: 1px solid ${(props) => props.theme.divider};
|
||||
z-index: 1;
|
||||
|
||||
${breakpoint("tablet")`
|
||||
display: flex;
|
||||
`};
|
||||
`;
|
||||
|
||||
const Title = styled(Flex)`
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
width: 0;
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
const Header = styled(Flex)`
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: 12px;
|
||||
color: ${(props) => props.theme.text};
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
export default observer(DocumentHistory);
|
||||
@@ -1,199 +0,0 @@
|
||||
// @flow
|
||||
import ArrowKeyNavigation from "boundless-arrow-key-navigation";
|
||||
import { action, observable } from "mobx";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { CloseIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { type Match, Redirect, type RouterHistory } from "react-router-dom";
|
||||
import { Waypoint } from "react-waypoint";
|
||||
import styled from "styled-components";
|
||||
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import { DEFAULT_PAGINATION_LIMIT } from "stores/BaseStore";
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
import RevisionsStore from "stores/RevisionsStore";
|
||||
|
||||
import Button from "components/Button";
|
||||
import Flex from "components/Flex";
|
||||
import PlaceholderList from "components/List/Placeholder";
|
||||
import Revision from "./components/Revision";
|
||||
import { documentHistoryUrl, documentUrl } from "utils/routeHelpers";
|
||||
|
||||
type Props = {
|
||||
match: Match,
|
||||
documents: DocumentsStore,
|
||||
revisions: RevisionsStore,
|
||||
history: RouterHistory,
|
||||
};
|
||||
|
||||
@observer
|
||||
class DocumentHistory extends React.Component<Props> {
|
||||
@observable isLoaded: boolean = false;
|
||||
@observable isFetching: boolean = false;
|
||||
@observable offset: number = 0;
|
||||
@observable allowLoadMore: boolean = true;
|
||||
@observable redirectTo: ?string;
|
||||
|
||||
async componentDidMount() {
|
||||
await this.loadMoreResults();
|
||||
this.selectFirstRevision();
|
||||
}
|
||||
|
||||
fetchResults = async () => {
|
||||
this.isFetching = true;
|
||||
|
||||
const limit = DEFAULT_PAGINATION_LIMIT;
|
||||
const results = await this.props.revisions.fetchPage({
|
||||
limit,
|
||||
offset: this.offset,
|
||||
documentId: this.props.match.params.documentSlug,
|
||||
});
|
||||
|
||||
if (
|
||||
results &&
|
||||
(results.length === 0 || results.length < DEFAULT_PAGINATION_LIMIT)
|
||||
) {
|
||||
this.allowLoadMore = false;
|
||||
} else {
|
||||
this.offset += DEFAULT_PAGINATION_LIMIT;
|
||||
}
|
||||
|
||||
this.isLoaded = true;
|
||||
this.isFetching = false;
|
||||
};
|
||||
|
||||
selectFirstRevision = () => {
|
||||
if (this.revisions.length) {
|
||||
const document = this.props.documents.getByUrl(
|
||||
this.props.match.params.documentSlug
|
||||
);
|
||||
if (!document) return;
|
||||
|
||||
this.props.history.replace(
|
||||
documentHistoryUrl(document, this.revisions[0].id)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@action
|
||||
loadMoreResults = async () => {
|
||||
// Don't paginate if there aren't more results or we’re in the middle of fetching
|
||||
if (!this.allowLoadMore || this.isFetching) return;
|
||||
await this.fetchResults();
|
||||
};
|
||||
|
||||
get revisions() {
|
||||
const document = this.props.documents.getByUrl(
|
||||
this.props.match.params.documentSlug
|
||||
);
|
||||
if (!document) return [];
|
||||
return this.props.revisions.getDocumentRevisions(document.id);
|
||||
}
|
||||
|
||||
onCloseHistory = () => {
|
||||
const document = this.props.documents.getByUrl(
|
||||
this.props.match.params.documentSlug
|
||||
);
|
||||
|
||||
this.redirectTo = documentUrl(document);
|
||||
};
|
||||
|
||||
render() {
|
||||
const document = this.props.documents.getByUrl(
|
||||
this.props.match.params.documentSlug
|
||||
);
|
||||
const showLoading = (!this.isLoaded && this.isFetching) || !document;
|
||||
|
||||
if (this.redirectTo) return <Redirect to={this.redirectTo} push />;
|
||||
|
||||
return (
|
||||
<Sidebar>
|
||||
<Wrapper column>
|
||||
<Header>
|
||||
<Title>History</Title>
|
||||
<Button
|
||||
icon={<CloseIcon />}
|
||||
onClick={this.onCloseHistory}
|
||||
borderOnHover
|
||||
neutral
|
||||
/>
|
||||
</Header>
|
||||
{showLoading ? (
|
||||
<Loading>
|
||||
<PlaceholderList count={5} />
|
||||
</Loading>
|
||||
) : (
|
||||
<ArrowKeyNavigation
|
||||
mode={ArrowKeyNavigation.mode.VERTICAL}
|
||||
defaultActiveChildIndex={0}
|
||||
>
|
||||
{this.revisions.map((revision, index) => (
|
||||
<Revision
|
||||
key={revision.id}
|
||||
revision={revision}
|
||||
document={document}
|
||||
showMenu={index !== 0}
|
||||
selected={this.props.match.params.revisionId === revision.id}
|
||||
/>
|
||||
))}
|
||||
</ArrowKeyNavigation>
|
||||
)}
|
||||
{this.allowLoadMore && (
|
||||
<Waypoint key={this.offset} onEnter={this.loadMoreResults} />
|
||||
)}
|
||||
</Wrapper>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Loading = styled.div`
|
||||
margin: 0 16px;
|
||||
`;
|
||||
|
||||
const Wrapper = styled(Flex)`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
min-width: ${(props) => props.theme.sidebarWidth}px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: none;
|
||||
`;
|
||||
|
||||
const Sidebar = styled(Flex)`
|
||||
display: none;
|
||||
background: ${(props) => props.theme.background};
|
||||
min-width: ${(props) => props.theme.sidebarWidth}px;
|
||||
border-left: 1px solid ${(props) => props.theme.divider};
|
||||
z-index: 1;
|
||||
|
||||
${breakpoint("tablet")`
|
||||
display: flex;
|
||||
`};
|
||||
`;
|
||||
|
||||
const Title = styled(Flex)`
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
width: 0;
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
const Header = styled(Flex)`
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid ${(props) => props.theme.divider};
|
||||
color: ${(props) => props.theme.text};
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
export default inject("documents", "revisions")(DocumentHistory);
|
||||
@@ -1,87 +0,0 @@
|
||||
// @flow
|
||||
import { format } from "date-fns";
|
||||
import * as React from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import styled, { withTheme } from "styled-components";
|
||||
|
||||
import Document from "models/Document";
|
||||
import Revision from "models/Revision";
|
||||
import Avatar from "components/Avatar";
|
||||
import Flex from "components/Flex";
|
||||
import Time from "components/Time";
|
||||
import RevisionMenu from "menus/RevisionMenu";
|
||||
import { type Theme } from "types";
|
||||
|
||||
import { documentHistoryUrl } from "utils/routeHelpers";
|
||||
|
||||
type Props = {
|
||||
theme: Theme,
|
||||
showMenu: boolean,
|
||||
selected: boolean,
|
||||
document: Document,
|
||||
revision: Revision,
|
||||
};
|
||||
|
||||
class RevisionListItem extends React.Component<Props> {
|
||||
render() {
|
||||
const { revision, document, showMenu, selected, theme } = this.props;
|
||||
|
||||
return (
|
||||
<StyledNavLink
|
||||
to={documentHistoryUrl(document, revision.id)}
|
||||
activeStyle={{ background: theme.primary, color: theme.white }}
|
||||
>
|
||||
<Author>
|
||||
<StyledAvatar src={revision.createdBy.avatarUrl} />{" "}
|
||||
{revision.createdBy.name}
|
||||
</Author>
|
||||
<Meta>
|
||||
<Time dateTime={revision.createdAt} tooltipDelay={250}>
|
||||
{format(Date.parse(revision.createdAt), "MMMM do, yyyy h:mm a")}
|
||||
</Time>
|
||||
</Meta>
|
||||
{showMenu && (
|
||||
<StyledRevisionMenu
|
||||
document={document}
|
||||
revision={revision}
|
||||
iconColor={selected ? theme.white : theme.textTertiary}
|
||||
/>
|
||||
)}
|
||||
</StyledNavLink>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const StyledAvatar = styled(Avatar)`
|
||||
border-color: transparent;
|
||||
margin-right: 4px;
|
||||
`;
|
||||
|
||||
const StyledRevisionMenu = styled(RevisionMenu)`
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 20px;
|
||||
`;
|
||||
|
||||
const StyledNavLink = styled(NavLink)`
|
||||
color: ${(props) => props.theme.text};
|
||||
display: block;
|
||||
padding: 8px 16px;
|
||||
font-size: 15px;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const Author = styled(Flex)`
|
||||
font-weight: 500;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
const Meta = styled.p`
|
||||
font-size: 14px;
|
||||
opacity: 0.75;
|
||||
margin: 0 0 2px;
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
export default withTheme(RevisionListItem);
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import DocumentHistory from "./DocumentHistory";
|
||||
export default DocumentHistory;
|
||||
@@ -66,6 +66,7 @@ function DocumentListItem(props: Props, ref) {
|
||||
const canStar =
|
||||
!document.isDraft && !document.isArchived && !document.isTemplate;
|
||||
const can = policies.abilities(currentTeam.id);
|
||||
const canCollection = policies.abilities(document.collectionId);
|
||||
|
||||
return (
|
||||
<DocumentLink
|
||||
@@ -126,7 +127,8 @@ function DocumentListItem(props: Props, ref) {
|
||||
{document.isTemplate &&
|
||||
!document.isArchived &&
|
||||
!document.isDeleted &&
|
||||
can.createDocument && (
|
||||
can.createDocument &&
|
||||
canCollection.update && (
|
||||
<>
|
||||
<Button
|
||||
as={Link}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
// @flow
|
||||
import {
|
||||
TrashIcon,
|
||||
ArchiveIcon,
|
||||
EditIcon,
|
||||
PublishIcon,
|
||||
MoveIcon,
|
||||
CheckboxIcon,
|
||||
} from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import Document from "models/Document";
|
||||
import Event from "models/Event";
|
||||
import Avatar from "components/Avatar";
|
||||
import Item, { Actions } from "components/List/Item";
|
||||
import Time from "components/Time";
|
||||
import RevisionMenu from "menus/RevisionMenu";
|
||||
import { documentHistoryUrl } from "utils/routeHelpers";
|
||||
|
||||
type Props = {|
|
||||
document: Document,
|
||||
event: Event,
|
||||
latest?: boolean,
|
||||
|};
|
||||
|
||||
const EventListItem = ({ event, latest, document }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const opts = { userName: event.actor.name };
|
||||
const isRevision = event.name === "revisions.create";
|
||||
let meta, icon, to;
|
||||
|
||||
switch (event.name) {
|
||||
case "revisions.create":
|
||||
case "documents.latest_version": {
|
||||
if (latest) {
|
||||
icon = <CheckboxIcon color="currentColor" size={16} checked />;
|
||||
meta = t("Latest version");
|
||||
to = documentHistoryUrl(document);
|
||||
break;
|
||||
} else {
|
||||
icon = <EditIcon color="currentColor" size={16} />;
|
||||
meta = t("{{userName}} edited", opts);
|
||||
to = documentHistoryUrl(document, event.modelId || "");
|
||||
break;
|
||||
}
|
||||
}
|
||||
case "documents.archive":
|
||||
icon = <ArchiveIcon color="currentColor" size={16} />;
|
||||
meta = t("{{userName}} archived", opts);
|
||||
break;
|
||||
case "documents.unarchive":
|
||||
meta = t("{{userName}} restored", opts);
|
||||
break;
|
||||
case "documents.delete":
|
||||
icon = <TrashIcon color="currentColor" size={16} />;
|
||||
meta = t("{{userName}} deleted", opts);
|
||||
break;
|
||||
case "documents.restore":
|
||||
meta = t("{{userName}} moved from trash", opts);
|
||||
break;
|
||||
case "documents.publish":
|
||||
icon = <PublishIcon color="currentColor" size={16} />;
|
||||
meta = t("{{userName}} published", opts);
|
||||
break;
|
||||
case "documents.move":
|
||||
icon = <MoveIcon color="currentColor" size={16} />;
|
||||
meta = t("{{userName}} moved", opts);
|
||||
break;
|
||||
default:
|
||||
console.warn("Unhandled event: ", event.name);
|
||||
}
|
||||
|
||||
if (!meta) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
small
|
||||
exact
|
||||
to={to}
|
||||
title={
|
||||
<Time
|
||||
dateTime={event.createdAt}
|
||||
tooltipDelay={250}
|
||||
format="MMMM do, h:mm a"
|
||||
relative={false}
|
||||
addSuffix
|
||||
/>
|
||||
}
|
||||
image={<Avatar src={event.actor?.avatarUrl} size={32} />}
|
||||
subtitle={
|
||||
<Subtitle>
|
||||
{icon}
|
||||
{meta}
|
||||
</Subtitle>
|
||||
}
|
||||
actions={
|
||||
isRevision ? (
|
||||
<RevisionMenu document={document} revisionId={event.modelId} />
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Subtitle = styled.span`
|
||||
svg {
|
||||
margin: -3px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ListItem = styled(Item)`
|
||||
border: 0;
|
||||
position: relative;
|
||||
margin: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
|
||||
img {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: 23px;
|
||||
width: 2px;
|
||||
height: calc(100% + 8px);
|
||||
background: ${(props) => props.theme.textSecondary};
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
&:nth-child(2)::before {
|
||||
height: 50%;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
&:last-child::before {
|
||||
height: 50%;
|
||||
}
|
||||
|
||||
&:first-child:last-child::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
${Actions} {
|
||||
opacity: 0.25;
|
||||
transition: opacity 100ms ease-in-out;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
${Actions} {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default EventListItem;
|
||||
@@ -22,7 +22,6 @@ import UiStore from "stores/UiStore";
|
||||
import ErrorSuspended from "scenes/ErrorSuspended";
|
||||
import KeyboardShortcuts from "scenes/KeyboardShortcuts";
|
||||
import Button from "components/Button";
|
||||
import DocumentHistory from "components/DocumentHistory";
|
||||
import Flex from "components/Flex";
|
||||
import Guide from "components/Guide";
|
||||
import { LoadingIndicatorBar } from "components/LoadingIndicator";
|
||||
@@ -38,6 +37,12 @@ import {
|
||||
newDocumentUrl,
|
||||
} from "utils/routeHelpers";
|
||||
|
||||
const DocumentHistory = React.lazy(() =>
|
||||
import(
|
||||
/* webpackChunkName: "document-history" */ "components/DocumentHistory"
|
||||
)
|
||||
);
|
||||
|
||||
type Props = {
|
||||
documents: DocumentsStore,
|
||||
children?: ?React.Node,
|
||||
@@ -154,12 +159,14 @@ class Layout extends React.Component<Props> {
|
||||
{this.props.children}
|
||||
</Content>
|
||||
|
||||
<Switch>
|
||||
<Route
|
||||
path={`/doc/${slug}/history/:revisionId?`}
|
||||
component={DocumentHistory}
|
||||
/>
|
||||
</Switch>
|
||||
<React.Suspense>
|
||||
<Switch>
|
||||
<Route
|
||||
path={`/doc/${slug}/history/:revisionId?`}
|
||||
component={DocumentHistory}
|
||||
/>
|
||||
</Switch>
|
||||
</React.Suspense>
|
||||
</Container>
|
||||
<Guide
|
||||
isOpen={this.keyboardShortcutsOpen}
|
||||
|
||||
+52
-23
@@ -1,41 +1,62 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import styled, { useTheme } from "styled-components";
|
||||
import Flex from "components/Flex";
|
||||
import NavLink from "components/NavLink";
|
||||
|
||||
type Props = {
|
||||
type Props = {|
|
||||
image?: React.Node,
|
||||
to?: string,
|
||||
title: React.Node,
|
||||
subtitle?: React.Node,
|
||||
actions?: React.Node,
|
||||
border?: boolean,
|
||||
small?: boolean,
|
||||
};
|
||||
|};
|
||||
|
||||
const ListItem = ({
|
||||
image,
|
||||
title,
|
||||
subtitle,
|
||||
actions,
|
||||
small,
|
||||
border,
|
||||
}: Props) => {
|
||||
const ListItem = (
|
||||
{ image, title, subtitle, actions, small, border, to, ...rest }: Props,
|
||||
ref
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
const compact = !subtitle;
|
||||
|
||||
return (
|
||||
<Wrapper compact={compact} $border={border}>
|
||||
const content = (selected) => (
|
||||
<>
|
||||
{image && <Image>{image}</Image>}
|
||||
<Content align={compact ? "center" : undefined} column={!compact}>
|
||||
<Content
|
||||
align={compact ? "center" : undefined}
|
||||
column={!compact}
|
||||
$selected={selected}
|
||||
>
|
||||
<Heading $small={small}>{title}</Heading>
|
||||
{subtitle && <Subtitle $small={small}>{subtitle}</Subtitle>}
|
||||
{subtitle && (
|
||||
<Subtitle $small={small} $selected={selected}>
|
||||
{subtitle}
|
||||
</Subtitle>
|
||||
)}
|
||||
</Content>
|
||||
{actions && <Actions>{actions}</Actions>}
|
||||
{actions && <Actions $selected={selected}>{actions}</Actions>}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper
|
||||
ref={ref}
|
||||
$border={border}
|
||||
activeStyle={{ background: theme.primary }}
|
||||
{...rest}
|
||||
as={to ? NavLink : undefined}
|
||||
to={to}
|
||||
>
|
||||
{to ? content : content(false)}
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const Wrapper = styled.li`
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
user-select: none;
|
||||
padding: ${(props) => (props.$border === false ? 0 : "8px 0")};
|
||||
margin: ${(props) => (props.$border === false ? "8px 0" : 0)};
|
||||
border-bottom: 1px solid
|
||||
@@ -57,28 +78,36 @@ const Image = styled(Flex)`
|
||||
`;
|
||||
|
||||
const Heading = styled.p`
|
||||
font-size: ${(props) => (props.$small ? 15 : 16)}px;
|
||||
font-size: ${(props) => (props.$small ? 14 : 16)}px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
line-height: 1.2;
|
||||
line-height: ${(props) => (props.$small ? 1.3 : 1.2)};
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
const Content = styled(Flex)`
|
||||
const Content = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
color: ${(props) => (props.$selected ? props.theme.white : props.theme.text)};
|
||||
`;
|
||||
|
||||
const Subtitle = styled.p`
|
||||
margin: 0;
|
||||
font-size: ${(props) => (props.$small ? 13 : 14)}px;
|
||||
color: ${(props) => props.theme.textTertiary};
|
||||
color: ${(props) =>
|
||||
props.$selected ? props.theme.white50 : props.theme.textTertiary};
|
||||
margin-top: -2px;
|
||||
`;
|
||||
|
||||
const Actions = styled.div`
|
||||
export const Actions = styled(Flex)`
|
||||
align-self: center;
|
||||
justify-content: center;
|
||||
margin-right: 4px;
|
||||
color: ${(props) =>
|
||||
props.$selected ? props.theme.white : props.theme.textSecondary};
|
||||
`;
|
||||
|
||||
export default ListItem;
|
||||
export default React.forwardRef<Props, HTMLDivElement>(ListItem);
|
||||
|
||||
@@ -1,39 +1,9 @@
|
||||
// @flow
|
||||
import { format, formatDistanceToNow } from "date-fns";
|
||||
import {
|
||||
enUS,
|
||||
de,
|
||||
faIR,
|
||||
fr,
|
||||
es,
|
||||
it,
|
||||
ja,
|
||||
ko,
|
||||
ptBR,
|
||||
pt,
|
||||
zhCN,
|
||||
zhTW,
|
||||
ru,
|
||||
} from "date-fns/locale";
|
||||
import { format as formatDate, formatDistanceToNow } from "date-fns";
|
||||
import * as React from "react";
|
||||
import Tooltip from "components/Tooltip";
|
||||
import useUserLocale from "hooks/useUserLocale";
|
||||
|
||||
const locales = {
|
||||
en_US: enUS,
|
||||
de_DE: de,
|
||||
es_ES: es,
|
||||
fa_IR: faIR,
|
||||
fr_FR: fr,
|
||||
it_IT: it,
|
||||
ja_JP: ja,
|
||||
ko_KR: ko,
|
||||
pt_BR: ptBR,
|
||||
pt_PT: pt,
|
||||
zh_CN: zhCN,
|
||||
zh_TW: zhTW,
|
||||
ru_RU: ru,
|
||||
};
|
||||
import { dateLocale } from "utils/i18n";
|
||||
|
||||
let callbacks = [];
|
||||
|
||||
@@ -57,6 +27,8 @@ type Props = {
|
||||
tooltipDelay?: number,
|
||||
addSuffix?: boolean,
|
||||
shorten?: boolean,
|
||||
relative?: boolean,
|
||||
format?: string,
|
||||
};
|
||||
|
||||
function LocaleTime({
|
||||
@@ -64,6 +36,8 @@ function LocaleTime({
|
||||
children,
|
||||
dateTime,
|
||||
shorten,
|
||||
format,
|
||||
relative,
|
||||
tooltipDelay,
|
||||
}: Props) {
|
||||
const userLocale = useUserLocale();
|
||||
@@ -82,25 +56,31 @@ function LocaleTime({
|
||||
};
|
||||
}, []);
|
||||
|
||||
let content = formatDistanceToNow(Date.parse(dateTime), {
|
||||
const locale = dateLocale(userLocale);
|
||||
let relativeContent = formatDistanceToNow(Date.parse(dateTime), {
|
||||
addSuffix,
|
||||
locale: userLocale ? locales[userLocale] : undefined,
|
||||
locale,
|
||||
});
|
||||
|
||||
if (shorten) {
|
||||
content = content
|
||||
relativeContent = relativeContent
|
||||
.replace("about", "")
|
||||
.replace("less than a minute ago", "just now")
|
||||
.replace("minute", "min");
|
||||
}
|
||||
|
||||
const tooltipContent = formatDate(
|
||||
Date.parse(dateTime),
|
||||
format || "MMMM do, yyyy h:mm a",
|
||||
{ locale }
|
||||
);
|
||||
|
||||
const content =
|
||||
children || relative !== false ? relativeContent : tooltipContent;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
tooltip={format(Date.parse(dateTime), "MMMM do, yyyy h:mm a")}
|
||||
delay={tooltipDelay}
|
||||
placement="bottom"
|
||||
>
|
||||
<time dateTime={dateTime}>{children || content}</time>
|
||||
<Tooltip tooltip={tooltipContent} delay={tooltipDelay} placement="bottom">
|
||||
<time dateTime={dateTime}>{content}</time>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// @flow
|
||||
import styled from "styled-components";
|
||||
|
||||
const MenuIconWrapper = styled.span`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 6px;
|
||||
margin-left: -4px;
|
||||
`;
|
||||
|
||||
export default MenuIconWrapper;
|
||||
@@ -0,0 +1,26 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { NavLink, Route, type Match } from "react-router-dom";
|
||||
|
||||
type Props = {
|
||||
children?: (match: Match) => React.Node,
|
||||
exact?: boolean,
|
||||
to: string,
|
||||
};
|
||||
|
||||
export default function NavLinkWithChildrenFunc({
|
||||
to,
|
||||
exact = false,
|
||||
children,
|
||||
...rest
|
||||
}: Props) {
|
||||
return (
|
||||
<Route path={to} exact={exact}>
|
||||
{({ match }) => (
|
||||
<NavLink {...rest} to={to} exact={exact}>
|
||||
{children ? children(match) : null}
|
||||
</NavLink>
|
||||
)}
|
||||
</Route>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ const Button = styled.button`
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: inherit;
|
||||
`;
|
||||
|
||||
export default React.forwardRef<any, typeof Button>(
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Document from "models/Document";
|
||||
import Event from "models/Event";
|
||||
import PaginatedList from "components/PaginatedList";
|
||||
import EventListItem from "./EventListItem";
|
||||
|
||||
type Props = {|
|
||||
events: Event[],
|
||||
document: Document,
|
||||
fetch: (options: ?Object) => Promise<void>,
|
||||
options?: Object,
|
||||
heading?: React.Node,
|
||||
empty?: React.Node,
|
||||
|};
|
||||
|
||||
const PaginatedEventList = React.memo<Props>(function PaginatedEventList({
|
||||
empty,
|
||||
heading,
|
||||
events,
|
||||
fetch,
|
||||
options,
|
||||
document,
|
||||
...rest
|
||||
}: Props) {
|
||||
return (
|
||||
<PaginatedList
|
||||
items={events}
|
||||
empty={empty}
|
||||
heading={heading}
|
||||
fetch={fetch}
|
||||
options={options}
|
||||
renderItem={(item, index) => (
|
||||
<EventListItem
|
||||
key={item.id}
|
||||
event={item}
|
||||
document={document}
|
||||
latest={index === 0}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
renderHeading={(name) => <Heading>{name}</Heading>}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const Heading = styled("h3")`
|
||||
font-size: 14px;
|
||||
padding: 0 12px;
|
||||
`;
|
||||
|
||||
export default PaginatedEventList;
|
||||
@@ -2,12 +2,15 @@
|
||||
import ArrowKeyNavigation from "boundless-arrow-key-navigation";
|
||||
import { isEqual } from "lodash";
|
||||
import { observable, action } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { observer, inject } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { withTranslation, type TFunction } from "react-i18next";
|
||||
import { Waypoint } from "react-waypoint";
|
||||
import AuthStore from "stores/AuthStore";
|
||||
import { DEFAULT_PAGINATION_LIMIT } from "stores/BaseStore";
|
||||
import DelayedMount from "components/DelayedMount";
|
||||
import PlaceholderList from "components/List/Placeholder";
|
||||
import { dateToHeading } from "utils/dates";
|
||||
|
||||
type Props = {
|
||||
fetch?: (options: ?Object) => Promise<void>,
|
||||
@@ -15,7 +18,10 @@ type Props = {
|
||||
heading?: React.Node,
|
||||
empty?: React.Node,
|
||||
items: any[],
|
||||
renderItem: (any) => React.Node,
|
||||
auth: AuthStore,
|
||||
renderItem: (any, index: number) => React.Node,
|
||||
renderHeading?: (name: React.Element<any> | string) => React.Node,
|
||||
t: TFunction,
|
||||
};
|
||||
|
||||
@observer
|
||||
@@ -101,8 +107,9 @@ class PaginatedList extends React.Component<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { items, heading, empty } = this.props;
|
||||
const { items, heading, auth, empty, renderHeading } = this.props;
|
||||
|
||||
let previousHeading = "";
|
||||
const showLoading =
|
||||
this.isFetching && !this.isFetchingMore && !this.isInitiallyLoaded;
|
||||
const showEmpty = !items.length && !showLoading;
|
||||
@@ -119,7 +126,41 @@ class PaginatedList extends React.Component<Props> {
|
||||
mode={ArrowKeyNavigation.mode.VERTICAL}
|
||||
defaultActiveChildIndex={0}
|
||||
>
|
||||
{items.slice(0, this.renderCount).map(this.props.renderItem)}
|
||||
{items.slice(0, this.renderCount).map((item, index) => {
|
||||
const children = this.props.renderItem(item, index);
|
||||
|
||||
// If there is no renderHeading method passed then no date
|
||||
// headings are rendered
|
||||
if (!renderHeading) {
|
||||
return children;
|
||||
}
|
||||
|
||||
// Our models have standard date fields, updatedAt > createdAt.
|
||||
// Get what a heading would look like for this item
|
||||
const currentDate =
|
||||
item.updatedAt || item.createdAt || previousHeading;
|
||||
const currentHeading = dateToHeading(
|
||||
currentDate,
|
||||
this.props.t,
|
||||
auth.user?.language
|
||||
);
|
||||
|
||||
// If the heading is different to any previous heading then we
|
||||
// should render it, otherwise the item can go under the previous
|
||||
// heading
|
||||
if (!previousHeading || currentHeading !== previousHeading) {
|
||||
previousHeading = currentHeading;
|
||||
|
||||
return (
|
||||
<React.Fragment key={item.id}>
|
||||
{renderHeading(currentHeading)}
|
||||
{children}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
})}
|
||||
</ArrowKeyNavigation>
|
||||
{this.allowLoadMore && (
|
||||
<Waypoint key={this.renderCount} onEnter={this.loadMoreResults} />
|
||||
@@ -136,4 +177,6 @@ class PaginatedList extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export default PaginatedList;
|
||||
export const Component = PaginatedList;
|
||||
|
||||
export default withTranslation()<PaginatedList>(inject("auth")(PaginatedList));
|
||||
|
||||
@@ -4,7 +4,7 @@ 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";
|
||||
import { Component as PaginatedList } from "./PaginatedList";
|
||||
|
||||
describe("PaginatedList", () => {
|
||||
const render = () => null;
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
import { observer } from "mobx-react";
|
||||
import {
|
||||
EditIcon,
|
||||
SearchIcon,
|
||||
ShapesIcon,
|
||||
HomeIcon,
|
||||
PlusIcon,
|
||||
SearchIcon,
|
||||
SettingsIcon,
|
||||
ShapesIcon,
|
||||
StarredIcon,
|
||||
} from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
@@ -25,6 +24,7 @@ import ArchiveLink from "./components/ArchiveLink";
|
||||
import Collections from "./components/Collections";
|
||||
import Section from "./components/Section";
|
||||
import SidebarLink from "./components/SidebarLink";
|
||||
import Starred from "./components/Starred";
|
||||
import TeamButton from "./components/TeamButton";
|
||||
import TrashLink from "./components/TrashLink";
|
||||
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||
@@ -109,12 +109,6 @@ function MainSidebar() {
|
||||
label={t("Search")}
|
||||
exact={false}
|
||||
/>
|
||||
<SidebarLink
|
||||
to="/starred"
|
||||
icon={<StarredIcon color="currentColor" />}
|
||||
exact={false}
|
||||
label={t("Starred")}
|
||||
/>
|
||||
{can.createDocument && (
|
||||
<SidebarLink
|
||||
to="/drafts"
|
||||
@@ -135,6 +129,7 @@ function MainSidebar() {
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
<Starred />
|
||||
<Section auto>
|
||||
<Collections
|
||||
onCreateCollection={handleCreateCollectionModalOpen}
|
||||
@@ -149,7 +144,11 @@ function MainSidebar() {
|
||||
exact={false}
|
||||
label={t("Templates")}
|
||||
active={
|
||||
documents.active ? documents.active.template : undefined
|
||||
documents.active
|
||||
? documents.active.isTemplate &&
|
||||
!documents.active.isDeleted &&
|
||||
!documents.active.isArchived
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<ArchiveLink documents={documents} />
|
||||
|
||||
@@ -159,6 +159,7 @@ function CollectionLink({
|
||||
/>
|
||||
}
|
||||
exact={false}
|
||||
depth={0.5}
|
||||
menu={
|
||||
<>
|
||||
{can.update && (
|
||||
@@ -198,7 +199,7 @@ function CollectionLink({
|
||||
activeDocument={activeDocument}
|
||||
prefetchDocument={prefetchDocument}
|
||||
canUpdate={canUpdate}
|
||||
depth={1.5}
|
||||
depth={2}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// @flow
|
||||
import fractionalIndex from "fractional-index";
|
||||
import { observer } from "mobx-react";
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import { PlusIcon, CollapsedIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useDrop } from "react-dnd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import Fade from "components/Fade";
|
||||
import Flex from "components/Flex";
|
||||
import useStores from "../../../hooks/useStores";
|
||||
import CollectionLink from "./CollectionLink";
|
||||
import DropCursor from "./DropCursor";
|
||||
import Header from "./Header";
|
||||
import PlaceholderCollections from "./PlaceholderCollections";
|
||||
import SidebarLink from "./SidebarLink";
|
||||
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||
@@ -25,6 +25,7 @@ function Collections({ onCreateCollection }: Props) {
|
||||
const [fetchError, setFetchError] = React.useState();
|
||||
const { ui, policies, documents, collections } = useStores();
|
||||
const { showToast } = useToasts();
|
||||
const [expanded, setExpanded] = React.useState(true);
|
||||
const isPreloaded: boolean = !!collections.orderedData.length;
|
||||
const { t } = useTranslation();
|
||||
const team = useCurrentTeam();
|
||||
@@ -99,6 +100,7 @@ function Collections({ onCreateCollection }: Props) {
|
||||
icon={<PlusIcon color="currentColor" />}
|
||||
label={`${t("New collection")}…`}
|
||||
exact
|
||||
depth={0.5}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -107,7 +109,11 @@ function Collections({ onCreateCollection }: Props) {
|
||||
if (!collections.isLoaded || fetchError) {
|
||||
return (
|
||||
<Flex column>
|
||||
<Header>{t("Collections")}</Header>
|
||||
<SidebarLink
|
||||
label={t("Collections")}
|
||||
icon={<Disclosure expanded={expanded} color="currentColor" />}
|
||||
disabled
|
||||
/>
|
||||
<PlaceholderCollections />
|
||||
</Flex>
|
||||
);
|
||||
@@ -115,10 +121,19 @@ function Collections({ onCreateCollection }: Props) {
|
||||
|
||||
return (
|
||||
<Flex column>
|
||||
<Header>{t("Collections")}</Header>
|
||||
{isPreloaded ? content : <Fade>{content}</Fade>}
|
||||
<SidebarLink
|
||||
onClick={() => setExpanded((prev) => !prev)}
|
||||
label={t("Collections")}
|
||||
icon={<Disclosure expanded={expanded} color="currentColor" />}
|
||||
/>
|
||||
{expanded && (isPreloaded ? content : <Fade>{content}</Fade>)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
const Disclosure = styled(CollapsedIcon)`
|
||||
transition: transform 100ms ease, fill 50ms !important;
|
||||
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
|
||||
`;
|
||||
|
||||
export default observer(Collections);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// @flow
|
||||
import { CollapsedIcon } from "outline-icons";
|
||||
import styled from "styled-components";
|
||||
|
||||
const Disclosure = styled(CollapsedIcon)`
|
||||
transition: transform 100ms ease, fill 50ms !important;
|
||||
position: absolute;
|
||||
left: -24px;
|
||||
|
||||
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
|
||||
`;
|
||||
|
||||
export default Disclosure;
|
||||
@@ -1,6 +1,5 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { CollapsedIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useDrag, useDrop } from "react-dnd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -8,6 +7,7 @@ import styled from "styled-components";
|
||||
import Collection from "models/Collection";
|
||||
import Document from "models/Document";
|
||||
import Fade from "components/Fade";
|
||||
import Disclosure from "./Disclosure";
|
||||
import DropCursor from "./DropCursor";
|
||||
import DropToImport from "./DropToImport";
|
||||
import EditableTitle from "./EditableTitle";
|
||||
@@ -210,7 +210,7 @@ function DocumentLink(
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ position: "relative" }} onDragLeave={resetHoverExpanding}>
|
||||
<Relative onDragLeave={resetHoverExpanding}>
|
||||
<Draggable
|
||||
key={node.id}
|
||||
ref={drag}
|
||||
@@ -244,6 +244,7 @@ function DocumentLink(
|
||||
depth={depth}
|
||||
exact={false}
|
||||
showActions={menuOpen}
|
||||
scrollIntoViewIfNeeded={!document?.isStarred}
|
||||
ref={ref}
|
||||
menu={
|
||||
document && !isMoving ? (
|
||||
@@ -263,7 +264,7 @@ function DocumentLink(
|
||||
{manualSort && (
|
||||
<DropCursor isActiveDrop={isOverReorder} innerRef={dropToReorder} />
|
||||
)}
|
||||
</div>
|
||||
</Relative>
|
||||
{expanded && !isDragging && (
|
||||
<>
|
||||
{node.children.map((childNode, index) => (
|
||||
@@ -285,17 +286,13 @@ function DocumentLink(
|
||||
);
|
||||
}
|
||||
|
||||
const Draggable = styled("div")`
|
||||
opacity: ${(props) => (props.$isDragging || props.$isMoving ? 0.5 : 1)};
|
||||
pointer-events: ${(props) => (props.$isMoving ? "none" : "all")};
|
||||
const Relative = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const Disclosure = styled(CollapsedIcon)`
|
||||
transition: transform 100ms ease, fill 50ms !important;
|
||||
position: absolute;
|
||||
left: -24px;
|
||||
|
||||
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
|
||||
const Draggable = styled.div`
|
||||
opacity: ${(props) => (props.$isDragging || props.$isMoving ? 0.5 : 1)};
|
||||
pointer-events: ${(props) => (props.$isMoving ? "none" : "all")};
|
||||
`;
|
||||
|
||||
const ObservedDocumentLink = observer(React.forwardRef(DocumentLink));
|
||||
|
||||
@@ -27,7 +27,7 @@ const Cursor = styled("div")`
|
||||
|
||||
width: 100%;
|
||||
height: 14px;
|
||||
${(props) => (props.from === "collections" ? "top: 15px;" : "bottom: -7px;")}
|
||||
${(props) => (props.from === "collections" ? "top: 25px;" : "bottom: -7px;")}
|
||||
background: transparent;
|
||||
|
||||
::after {
|
||||
|
||||
@@ -5,10 +5,11 @@ import Flex from "components/Flex";
|
||||
const Header = styled(Flex)`
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
user-select: none;
|
||||
text-transform: uppercase;
|
||||
color: ${(props) => props.theme.sidebarText};
|
||||
letter-spacing: 0.04em;
|
||||
margin: 4px 16px;
|
||||
margin: 4px 12px;
|
||||
`;
|
||||
|
||||
export default Header;
|
||||
|
||||
@@ -31,6 +31,7 @@ type Props = {|
|
||||
activeClassName?: String,
|
||||
activeStyle?: Object,
|
||||
className?: string,
|
||||
scrollIntoViewIfNeeded?: boolean,
|
||||
exact?: boolean,
|
||||
isActive?: any,
|
||||
location?: Location,
|
||||
@@ -52,6 +53,7 @@ const NavLink = ({
|
||||
location: locationProp,
|
||||
strict,
|
||||
style: styleProp,
|
||||
scrollIntoViewIfNeeded,
|
||||
to,
|
||||
...rest
|
||||
}: Props) => {
|
||||
@@ -83,13 +85,13 @@ const NavLink = ({
|
||||
const style = isActive ? { ...styleProp, ...activeStyle } : styleProp;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isActive && linkRef.current) {
|
||||
if (isActive && linkRef.current && scrollIntoViewIfNeeded !== false) {
|
||||
scrollIntoView(linkRef.current, {
|
||||
scrollMode: "if-needed",
|
||||
behavior: "instant",
|
||||
});
|
||||
}
|
||||
}, [linkRef, isActive]);
|
||||
}, [linkRef, scrollIntoViewIfNeeded, isActive]);
|
||||
|
||||
const props = {
|
||||
"aria-current": (isActive && ariaCurrent) || null,
|
||||
|
||||
@@ -15,6 +15,7 @@ function PlaceholderCollections() {
|
||||
|
||||
const Wrapper = styled.div`
|
||||
margin: 4px 16px;
|
||||
margin-left: 40px;
|
||||
width: 75%;
|
||||
`;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import Flex from "components/Flex";
|
||||
const Section = styled(Flex)`
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
margin: 0 8px 20px;
|
||||
margin: 0 8px 12px;
|
||||
min-width: ${(props) => props.theme.sidebarMinWidth}px;
|
||||
flex-shrink: 0;
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ type Props = {
|
||||
theme: Theme,
|
||||
exact?: boolean,
|
||||
depth?: number,
|
||||
scrollIntoViewIfNeeded?: boolean,
|
||||
};
|
||||
|
||||
function SidebarLink(
|
||||
@@ -49,12 +50,13 @@ function SidebarLink(
|
||||
history,
|
||||
match,
|
||||
className,
|
||||
scrollIntoViewIfNeeded,
|
||||
}: Props,
|
||||
ref
|
||||
) {
|
||||
const style = React.useMemo(() => {
|
||||
return {
|
||||
paddingLeft: `${(depth || 0) * 16 + 16}px`,
|
||||
paddingLeft: `${(depth || 0) * 16 + 12}px`,
|
||||
};
|
||||
}, [depth]);
|
||||
|
||||
@@ -73,6 +75,7 @@ function SidebarLink(
|
||||
<>
|
||||
<Link
|
||||
$isActiveDrop={isActiveDrop}
|
||||
scrollIntoViewIfNeeded={scrollIntoViewIfNeeded}
|
||||
activeStyle={isActiveDrop ? activeDropStyle : activeStyle}
|
||||
style={active ? activeStyle : style}
|
||||
onClick={onClick}
|
||||
@@ -131,6 +134,7 @@ const Link = styled(NavLink)`
|
||||
padding: 6px 16px;
|
||||
border-radius: 4px;
|
||||
transition: background 50ms, color 50ms;
|
||||
user-select: none;
|
||||
background: ${(props) =>
|
||||
props.$isActiveDrop ? props.theme.slateDark : "inherit"};
|
||||
color: ${(props) =>
|
||||
@@ -156,13 +160,11 @@ const Link = styled(NavLink)`
|
||||
`}
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover + ${Actions},
|
||||
&:active + ${Actions} {
|
||||
display: inline-flex;
|
||||
&:hover + ${Actions}, &:active + ${Actions} {
|
||||
display: inline-flex;
|
||||
|
||||
svg {
|
||||
opacity: 0.75;
|
||||
}
|
||||
svg {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { CollapsedIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import Flex from "components/Flex";
|
||||
import PlaceholderCollections from "./PlaceholderCollections";
|
||||
import Section from "./Section";
|
||||
import SidebarLink from "./SidebarLink";
|
||||
import StarredLink from "./StarredLink";
|
||||
import useStores from "hooks/useStores";
|
||||
import useToasts from "hooks/useToasts";
|
||||
|
||||
const STARRED_PAGINATION_LIMIT = 10;
|
||||
const STARRED = "STARRED";
|
||||
|
||||
function Starred() {
|
||||
const [isFetching, setIsFetching] = React.useState(false);
|
||||
const [fetchError, setFetchError] = React.useState();
|
||||
const [expanded, setExpanded] = React.useState(true);
|
||||
const [show, setShow] = React.useState("Nothing");
|
||||
const [offset, setOffset] = React.useState(0);
|
||||
const [upperBound, setUpperBound] = React.useState(STARRED_PAGINATION_LIMIT);
|
||||
const { showToast } = useToasts();
|
||||
const { documents } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const { fetchStarred, starred } = documents;
|
||||
|
||||
const fetchResults = React.useCallback(async () => {
|
||||
try {
|
||||
setIsFetching(true);
|
||||
await fetchStarred({
|
||||
limit: STARRED_PAGINATION_LIMIT,
|
||||
offset,
|
||||
});
|
||||
} catch (error) {
|
||||
showToast(t("Starred documents could not be loaded"), {
|
||||
type: "error",
|
||||
});
|
||||
setFetchError(error);
|
||||
} finally {
|
||||
setIsFetching(false);
|
||||
}
|
||||
}, [fetchStarred, offset, showToast, t]);
|
||||
|
||||
useEffect(() => {
|
||||
let stateInLocal;
|
||||
|
||||
try {
|
||||
stateInLocal = localStorage.getItem(STARRED);
|
||||
} catch (_) {
|
||||
// no-op Safari private mode
|
||||
}
|
||||
|
||||
if (!stateInLocal) {
|
||||
localStorage.setItem(STARRED, expanded ? "true" : "false");
|
||||
} else {
|
||||
setExpanded(stateInLocal === "true");
|
||||
}
|
||||
}, [expanded]);
|
||||
|
||||
useEffect(() => {
|
||||
setOffset(starred.length);
|
||||
if (starred.length <= STARRED_PAGINATION_LIMIT) {
|
||||
setShow("Nothing");
|
||||
} else if (starred.length >= upperBound) {
|
||||
setShow("More");
|
||||
} else if (starred.length < upperBound) {
|
||||
setShow("Less");
|
||||
}
|
||||
}, [starred, upperBound]);
|
||||
|
||||
useEffect(() => {
|
||||
if (offset === 0) {
|
||||
fetchResults();
|
||||
}
|
||||
}, [fetchResults, offset]);
|
||||
|
||||
const handleShowMore = React.useCallback(
|
||||
async (ev) => {
|
||||
setUpperBound(
|
||||
(previousUpperBound) => previousUpperBound + STARRED_PAGINATION_LIMIT
|
||||
);
|
||||
await fetchResults();
|
||||
},
|
||||
[fetchResults]
|
||||
);
|
||||
|
||||
const handleShowLess = React.useCallback((ev) => {
|
||||
setUpperBound(STARRED_PAGINATION_LIMIT);
|
||||
setShow("More");
|
||||
}, []);
|
||||
|
||||
const handleExpandClick = React.useCallback(
|
||||
(ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
try {
|
||||
localStorage.setItem(STARRED, !expanded ? "true" : "false");
|
||||
} catch (_) {
|
||||
// no-op Safari private mode
|
||||
}
|
||||
setExpanded((prev) => !prev);
|
||||
},
|
||||
[expanded]
|
||||
);
|
||||
|
||||
const content = starred.slice(0, upperBound).map((document, index) => {
|
||||
return (
|
||||
<StarredLink
|
||||
key={document.id}
|
||||
documentId={document.id}
|
||||
collectionId={document.collectionId}
|
||||
to={document.url}
|
||||
title={document.title}
|
||||
url={document.url}
|
||||
depth={2}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
if (!starred.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<Flex column>
|
||||
<SidebarLink
|
||||
onClick={handleExpandClick}
|
||||
label={t("Starred")}
|
||||
icon={<Disclosure expanded={expanded} color="currentColor" />}
|
||||
/>
|
||||
{expanded && (
|
||||
<>
|
||||
{content}
|
||||
{show === "More" && !isFetching && (
|
||||
<SidebarLink
|
||||
onClick={handleShowMore}
|
||||
label={`${t("Show more")}…`}
|
||||
depth={2}
|
||||
/>
|
||||
)}
|
||||
{show === "Less" && !isFetching && (
|
||||
<SidebarLink
|
||||
onClick={handleShowLess}
|
||||
label={`${t("Show less")}…`}
|
||||
depth={2}
|
||||
/>
|
||||
)}
|
||||
{(isFetching || fetchError) && (
|
||||
<Flex column>
|
||||
<PlaceholderCollections />
|
||||
</Flex>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
const Disclosure = styled(CollapsedIcon)`
|
||||
transition: transform 100ms ease, fill 50ms !important;
|
||||
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
|
||||
`;
|
||||
|
||||
export default observer(Starred);
|
||||
@@ -0,0 +1,102 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
import Fade from "components/Fade";
|
||||
import useStores from "../../../hooks/useStores";
|
||||
import Disclosure from "./Disclosure";
|
||||
import SidebarLink from "./SidebarLink";
|
||||
import useBoolean from "hooks/useBoolean";
|
||||
import DocumentMenu from "menus/DocumentMenu";
|
||||
|
||||
type Props = {|
|
||||
depth: number,
|
||||
title: string,
|
||||
to: string,
|
||||
documentId: string,
|
||||
collectionId: string,
|
||||
|};
|
||||
|
||||
function StarredLink({ depth, title, to, documentId, collectionId }: Props) {
|
||||
const { collections, documents } = useStores();
|
||||
const collection = collections.get(collectionId);
|
||||
const document = documents.get(documentId);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
|
||||
|
||||
const childDocuments = collection
|
||||
? collection.getDocumentChildren(documentId)
|
||||
: [];
|
||||
|
||||
const hasChildDocuments = childDocuments.length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
if (!document) {
|
||||
await documents.fetch(documentId);
|
||||
}
|
||||
}
|
||||
load();
|
||||
}, [collection, collectionId, collections, document, documentId, documents]);
|
||||
|
||||
const handleDisclosureClick = React.useCallback((ev: SyntheticEvent<>) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
setExpanded((prevExpanded) => !prevExpanded);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Relative>
|
||||
<SidebarLink
|
||||
depth={depth}
|
||||
to={to}
|
||||
label={
|
||||
<>
|
||||
{hasChildDocuments && (
|
||||
<Disclosure
|
||||
expanded={expanded}
|
||||
onClick={handleDisclosureClick}
|
||||
/>
|
||||
)}
|
||||
{title}
|
||||
</>
|
||||
}
|
||||
exact={false}
|
||||
showActions={menuOpen}
|
||||
menu={
|
||||
document ? (
|
||||
<Fade>
|
||||
<DocumentMenu
|
||||
document={document}
|
||||
onOpen={handleMenuOpen}
|
||||
onClose={handleMenuClose}
|
||||
/>
|
||||
</Fade>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</Relative>
|
||||
{expanded &&
|
||||
childDocuments.map((childDocument) => (
|
||||
<ObserveredStarredLink
|
||||
key={childDocument.id}
|
||||
depth={depth + 1}
|
||||
title={childDocument.title}
|
||||
to={childDocument.url}
|
||||
documentId={childDocument.id}
|
||||
collectionId={collectionId}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const Relative = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const ObserveredStarredLink = observer(StarredLink);
|
||||
|
||||
export default ObserveredStarredLink;
|
||||
+4
-17
@@ -1,25 +1,13 @@
|
||||
// @flow
|
||||
import { m } from "framer-motion";
|
||||
import * as React from "react";
|
||||
import { NavLink, Route } from "react-router-dom";
|
||||
import styled, { withTheme } from "styled-components";
|
||||
import { type Theme } from "types";
|
||||
import styled, { useTheme } from "styled-components";
|
||||
import NavLinkWithChildrenFunc from "components/NavLink";
|
||||
|
||||
type Props = {
|
||||
theme: Theme,
|
||||
children: React.Node,
|
||||
};
|
||||
|
||||
const NavLinkWithChildrenFunc = ({ to, exact = false, children, ...rest }) => (
|
||||
<Route path={to} exact={exact}>
|
||||
{({ match }) => (
|
||||
<NavLink to={to} exact={exact} {...rest}>
|
||||
{children(match)}
|
||||
</NavLink>
|
||||
)}
|
||||
</Route>
|
||||
);
|
||||
|
||||
const TabLink = styled(NavLinkWithChildrenFunc)`
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
@@ -53,7 +41,8 @@ const transition = {
|
||||
damping: 30,
|
||||
};
|
||||
|
||||
function Tab({ theme, children, ...rest }: Props) {
|
||||
export default function Tab({ children, ...rest }: Props) {
|
||||
const theme = useTheme();
|
||||
const activeStyle = {
|
||||
color: theme.textSecondary,
|
||||
};
|
||||
@@ -75,5 +64,3 @@ function Tab({ theme, children, ...rest }: Props) {
|
||||
</TabLink>
|
||||
);
|
||||
}
|
||||
|
||||
export default withTheme(Tab);
|
||||
|
||||
+55
-16
@@ -1,4 +1,5 @@
|
||||
// @flow
|
||||
import { isEqual } from "lodash";
|
||||
import { observer } from "mobx-react";
|
||||
import { CollapsedIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
@@ -24,6 +25,7 @@ export type Props = {|
|
||||
onChangePage: (index: number) => void,
|
||||
onChangeSort: (sort: ?string, direction: "ASC" | "DESC") => void,
|
||||
columns: any,
|
||||
defaultSortDirection: "ASC" | "DESC",
|
||||
|};
|
||||
|
||||
function Table({
|
||||
@@ -39,6 +41,7 @@ function Table({
|
||||
topRef,
|
||||
onChangeSort,
|
||||
onChangePage,
|
||||
defaultSortDirection,
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
@@ -62,32 +65,52 @@ function Table({
|
||||
autoResetPage: false,
|
||||
pageCount: totalPages,
|
||||
initialState: {
|
||||
sortBy: [{ id: defaultSort, desc: false }],
|
||||
sortBy: [
|
||||
{
|
||||
id: defaultSort,
|
||||
desc: defaultSortDirection === "DESC" ? true : false,
|
||||
},
|
||||
],
|
||||
pageSize,
|
||||
pageIndex: page,
|
||||
},
|
||||
stateReducer: (newState, action, prevState) => {
|
||||
if (!isEqual(newState.sortBy, prevState.sortBy)) {
|
||||
return { ...newState, pageIndex: 0 };
|
||||
}
|
||||
return newState;
|
||||
},
|
||||
},
|
||||
useSortBy,
|
||||
usePagination
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
onChangePage(pageIndex);
|
||||
}, [pageIndex]);
|
||||
const prevSortBy = React.useRef(sortBy);
|
||||
|
||||
React.useEffect(() => {
|
||||
onChangePage(0);
|
||||
onChangeSort(
|
||||
sortBy.length ? sortBy[0].id : undefined,
|
||||
sortBy.length && sortBy[0].desc ? "DESC" : "ASC"
|
||||
);
|
||||
}, [sortBy]);
|
||||
if (!isEqual(sortBy, prevSortBy.current)) {
|
||||
prevSortBy.current = sortBy;
|
||||
onChangePage(0);
|
||||
onChangeSort(
|
||||
sortBy.length ? sortBy[0].id : undefined,
|
||||
!sortBy.length ? defaultSortDirection : sortBy[0].desc ? "DESC" : "ASC"
|
||||
);
|
||||
}
|
||||
}, [defaultSortDirection, onChangePage, onChangeSort, sortBy]);
|
||||
|
||||
const handleNextPage = () => {
|
||||
nextPage();
|
||||
onChangePage(pageIndex + 1);
|
||||
};
|
||||
|
||||
const handlePreviousPage = () => {
|
||||
previousPage();
|
||||
onChangePage(pageIndex - 1);
|
||||
};
|
||||
|
||||
const isEmpty = !isLoading && data.length === 0;
|
||||
const showPlaceholder = isLoading && data.length === 0;
|
||||
|
||||
console.log({ canNextPage, pageIndex, totalPages, rows, data });
|
||||
|
||||
return (
|
||||
<>
|
||||
<Anchor ref={topRef} />
|
||||
@@ -142,12 +165,12 @@ function Table({
|
||||
>
|
||||
{/* Note: the page > 0 check shouldn't be needed here but is */}
|
||||
{canPreviousPage && page > 0 && (
|
||||
<Button onClick={previousPage} neutral>
|
||||
<Button onClick={handlePreviousPage} neutral>
|
||||
{t("Previous page")}
|
||||
</Button>
|
||||
)}
|
||||
{canNextPage && (
|
||||
<Button onClick={nextPage} neutral>
|
||||
<Button onClick={handleNextPage} neutral>
|
||||
{t("Next page")}
|
||||
</Button>
|
||||
)}
|
||||
@@ -209,7 +232,7 @@ const SortWrapper = styled(Flex)`
|
||||
`;
|
||||
|
||||
const Cell = styled.td`
|
||||
padding: 8px 0;
|
||||
padding: 6px;
|
||||
border-bottom: 1px solid ${(props) => props.theme.divider};
|
||||
font-size: 14px;
|
||||
|
||||
@@ -226,6 +249,14 @@ const Cell = styled.td`
|
||||
`;
|
||||
|
||||
const Row = styled.tr`
|
||||
${Cell} {
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
&:last-child {
|
||||
${Cell} {
|
||||
border-bottom: 0;
|
||||
@@ -237,7 +268,7 @@ const Head = styled.th`
|
||||
text-align: left;
|
||||
position: sticky;
|
||||
top: 54px;
|
||||
padding: 6px 0;
|
||||
padding: 6px;
|
||||
border-bottom: 1px solid ${(props) => props.theme.divider};
|
||||
background: ${(props) => props.theme.background};
|
||||
transition: ${(props) => props.theme.backgroundTransition};
|
||||
@@ -245,6 +276,14 @@ const Head = styled.th`
|
||||
color: ${(props) => props.theme.textSecondary};
|
||||
font-weight: 500;
|
||||
z-index: 1;
|
||||
|
||||
:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export default observer(Table);
|
||||
|
||||
@@ -11,6 +11,7 @@ type Props = {
|
||||
children?: React.Node,
|
||||
tooltipDelay?: number,
|
||||
addSuffix?: boolean,
|
||||
format?: string,
|
||||
shorten?: boolean,
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import Image from "components/Image";
|
||||
import Frame from "./components/Frame";
|
||||
|
||||
const URL_REGEX = new RegExp("^https://viewer.diagrams.net/.*(title=\\w+)?");
|
||||
|
||||
type Props = {|
|
||||
attrs: {|
|
||||
href: string,
|
||||
matches: string[],
|
||||
|},
|
||||
|};
|
||||
|
||||
export default class Diagrams extends React.Component<Props> {
|
||||
static ENABLED = [URL_REGEX];
|
||||
|
||||
get embedUrl() {
|
||||
return this.props.attrs.matches[0];
|
||||
}
|
||||
|
||||
get title() {
|
||||
let title = "Diagrams.net";
|
||||
const url = new URL(this.embedUrl);
|
||||
const documentTitle = url.searchParams.get("title");
|
||||
|
||||
if (documentTitle) {
|
||||
title += ` (${documentTitle})`;
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Frame
|
||||
{...this.props}
|
||||
src={this.embedUrl}
|
||||
title={this.title}
|
||||
border
|
||||
icon={
|
||||
<Image
|
||||
src="/images/diagrams.png"
|
||||
alt="Diagrams.net"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
import Diagrams from "./Diagrams";
|
||||
|
||||
describe("Diagrams", () => {
|
||||
const match = Diagrams.ENABLED[0];
|
||||
|
||||
test("to be enabled on viewer link", () => {
|
||||
expect(
|
||||
"https://viewer.diagrams.net/?target=blank&nav=1#ABCDefgh_A12345-6789".match(
|
||||
match
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test("to not be enabled elsewhere", () => {
|
||||
expect("https://app.diagrams.net/#ABCDefgh_A12345-6789".match(match)).toBe(
|
||||
null
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import Image from "components/Image";
|
||||
import Frame from "./components/Frame";
|
||||
|
||||
const URL_REGEX = new RegExp(
|
||||
"^https?://drive.google.com/file/d/(.*)/(preview|view).?usp=sharing$"
|
||||
"^https?://drive.google.com/file/d/(.*)/(preview|view).?usp=sharing(.*)"
|
||||
);
|
||||
|
||||
type Props = {|
|
||||
@@ -29,7 +29,7 @@ export default class GoogleDrive extends React.Component<Props> {
|
||||
height={16}
|
||||
/>
|
||||
}
|
||||
title="Google Drive Embed"
|
||||
title="Google Drive"
|
||||
canonicalUrl={this.props.attrs.href}
|
||||
border
|
||||
/>
|
||||
|
||||
@@ -3,6 +3,7 @@ import GoogleDrive from "./GoogleDrive";
|
||||
|
||||
describe("GoogleDrive", () => {
|
||||
const match = GoogleDrive.ENABLED[0];
|
||||
|
||||
test("to be enabled on share link", () => {
|
||||
expect(
|
||||
"https://drive.google.com/file/d/1ohkOgmE8MiNx68u6ynBfYkgjeKu_x3ZK/view?usp=sharing".match(
|
||||
@@ -14,6 +15,11 @@ describe("GoogleDrive", () => {
|
||||
match
|
||||
)
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
"https://drive.google.com/file/d/1ohkOgmE8MiNx68u6ynBfYkgjeKu_x3ZK/preview?usp=sharing&resourceKey=BG8k4dEt1p2gisnVdlaSpA".match(
|
||||
match
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test("to not be enabled elsewhere", () => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import Cawemo from "./Cawemo";
|
||||
import ClickUp from "./ClickUp";
|
||||
import Codepen from "./Codepen";
|
||||
import Descript from "./Descript";
|
||||
import Diagrams from "./Diagrams";
|
||||
import Figma from "./Figma";
|
||||
import Framer from "./Framer";
|
||||
import Gist from "./Gist";
|
||||
@@ -115,6 +116,13 @@ export default [
|
||||
component: Gist,
|
||||
matcher: matcher(Gist),
|
||||
},
|
||||
{
|
||||
title: "Diagrams.net",
|
||||
keywords: "diagrams drawio",
|
||||
icon: () => <Img src="/images/diagrams.png" />,
|
||||
component: Diagrams,
|
||||
matcher: matcher(Diagrams),
|
||||
},
|
||||
{
|
||||
title: "Google Drawings",
|
||||
keywords: "drawings",
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { getCookie } from "tiny-cookie";
|
||||
|
||||
type Session = {|
|
||||
url: string,
|
||||
logoUrl: string,
|
||||
name: string,
|
||||
teamId: string,
|
||||
|};
|
||||
|
||||
function loadSessionsFromCookie(): Session[] {
|
||||
const sessions = JSON.parse(getCookie("sessions") || "{}");
|
||||
|
||||
return Object.keys(sessions).map((teamId) => ({
|
||||
teamId,
|
||||
...sessions[teamId],
|
||||
}));
|
||||
}
|
||||
|
||||
export default function useSessions() {
|
||||
const [sessions, setSessions] = React.useState(loadSessionsFromCookie);
|
||||
|
||||
const reload = React.useCallback(() => {
|
||||
setSessions(loadSessionsFromCookie());
|
||||
}, []);
|
||||
|
||||
return [sessions, reload];
|
||||
}
|
||||
+103
-81
@@ -1,84 +1,42 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { SunIcon, MoonIcon } from "outline-icons";
|
||||
import { MoonIcon, SunIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useMenuState, MenuButton } from "reakit/Menu";
|
||||
import { MenuButton, useMenuState } from "reakit/Menu";
|
||||
import styled from "styled-components";
|
||||
import {
|
||||
developers,
|
||||
changelog,
|
||||
developers,
|
||||
githubIssuesUrl,
|
||||
mailToUrl,
|
||||
settings,
|
||||
} from "shared/utils/routeHelpers";
|
||||
import KeyboardShortcuts from "scenes/KeyboardShortcuts";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import MenuItem, { MenuAnchor } from "components/ContextMenu/MenuItem";
|
||||
import Separator from "components/ContextMenu/Separator";
|
||||
import Flex from "components/Flex";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
import Guide from "components/Guide";
|
||||
import useBoolean from "hooks/useBoolean";
|
||||
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||
import usePrevious from "hooks/usePrevious";
|
||||
import useSessions from "hooks/useSessions";
|
||||
import useStores from "hooks/useStores";
|
||||
|
||||
type Props = {|
|
||||
children: (props: any) => React.Node,
|
||||
|};
|
||||
|
||||
const AppearanceMenu = React.forwardRef((props, ref) => {
|
||||
const { ui } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const menu = useMenuState();
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuButton ref={ref} {...menu} {...props} onClick={menu.show}>
|
||||
{(props) => (
|
||||
<MenuAnchor {...props}>
|
||||
<ChangeTheme justify="space-between">
|
||||
{t("Appearance")}
|
||||
{ui.resolvedTheme === "light" ? <SunIcon /> : <MoonIcon />}
|
||||
</ChangeTheme>
|
||||
</MenuAnchor>
|
||||
)}
|
||||
</MenuButton>
|
||||
<ContextMenu {...menu} aria-label={t("Appearance")}>
|
||||
<MenuItem
|
||||
{...menu}
|
||||
onClick={() => ui.setTheme("system")}
|
||||
selected={ui.theme === "system"}
|
||||
>
|
||||
{t("System")}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
{...menu}
|
||||
onClick={() => ui.setTheme("light")}
|
||||
selected={ui.theme === "light"}
|
||||
>
|
||||
{t("Light")}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
{...menu}
|
||||
onClick={() => ui.setTheme("dark")}
|
||||
selected={ui.theme === "dark"}
|
||||
>
|
||||
{t("Dark")}
|
||||
</MenuItem>
|
||||
</ContextMenu>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
function AccountMenu(props: Props) {
|
||||
const [sessions] = useSessions();
|
||||
const menu = useMenuState({
|
||||
unstable_offset: [8, 0],
|
||||
placement: "bottom-start",
|
||||
modal: true,
|
||||
});
|
||||
const { auth, ui } = useStores();
|
||||
const previousTheme = usePrevious(ui.theme);
|
||||
const { theme, resolvedTheme } = ui;
|
||||
const team = useCurrentTeam();
|
||||
const previousTheme = usePrevious(theme);
|
||||
const { t } = useTranslation();
|
||||
const [
|
||||
keyboardShortcutsOpen,
|
||||
@@ -87,10 +45,96 @@ function AccountMenu(props: Props) {
|
||||
] = useBoolean();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (ui.theme !== previousTheme) {
|
||||
if (theme !== previousTheme) {
|
||||
menu.hide();
|
||||
}
|
||||
}, [menu, ui.theme, previousTheme]);
|
||||
}, [menu, theme, previousTheme]);
|
||||
|
||||
const items = React.useMemo(() => {
|
||||
const otherSessions = sessions.filter(
|
||||
(session) => session.teamId !== team.id && session.url !== team.url
|
||||
);
|
||||
|
||||
return [
|
||||
{
|
||||
title: t("Settings"),
|
||||
to: settings(),
|
||||
},
|
||||
{
|
||||
title: t("Keyboard shortcuts"),
|
||||
onClick: handleKeyboardShortcutsOpen,
|
||||
},
|
||||
{
|
||||
title: t("API documentation"),
|
||||
href: developers(),
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
title: t("Changelog"),
|
||||
href: changelog(),
|
||||
},
|
||||
{
|
||||
title: t("Send us feedback"),
|
||||
href: mailToUrl(),
|
||||
},
|
||||
{
|
||||
title: t("Report a bug"),
|
||||
href: githubIssuesUrl(),
|
||||
},
|
||||
{
|
||||
title: t("Appearance"),
|
||||
icon: resolvedTheme === "light" ? <SunIcon /> : <MoonIcon />,
|
||||
items: [
|
||||
{
|
||||
title: t("System"),
|
||||
onClick: () => ui.setTheme("system"),
|
||||
selected: theme === "system",
|
||||
},
|
||||
{
|
||||
title: t("Light"),
|
||||
onClick: () => ui.setTheme("light"),
|
||||
selected: theme === "light",
|
||||
},
|
||||
{
|
||||
title: t("Dark"),
|
||||
onClick: () => ui.setTheme("dark"),
|
||||
selected: theme === "dark",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
...(otherSessions.length
|
||||
? [
|
||||
{
|
||||
title: t("Switch team"),
|
||||
items: otherSessions.map((session) => ({
|
||||
title: session.name,
|
||||
icon: <Logo alt={session.name} src={session.logoUrl} />,
|
||||
href: session.url,
|
||||
})),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: t("Log out"),
|
||||
onClick: auth.logout,
|
||||
},
|
||||
];
|
||||
}, [
|
||||
auth.logout,
|
||||
team.id,
|
||||
team.url,
|
||||
sessions,
|
||||
handleKeyboardShortcutsOpen,
|
||||
resolvedTheme,
|
||||
theme,
|
||||
t,
|
||||
ui,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -103,38 +147,16 @@ function AccountMenu(props: Props) {
|
||||
</Guide>
|
||||
<MenuButton {...menu}>{props.children}</MenuButton>
|
||||
<ContextMenu {...menu} aria-label={t("Account")}>
|
||||
<MenuItem {...menu} as={Link} to={settings()}>
|
||||
{t("Settings")}
|
||||
</MenuItem>
|
||||
<MenuItem {...menu} onClick={handleKeyboardShortcutsOpen}>
|
||||
{t("Keyboard shortcuts")}
|
||||
</MenuItem>
|
||||
<MenuItem {...menu} href={developers()} target="_blank">
|
||||
{t("API documentation")}
|
||||
</MenuItem>
|
||||
<Separator {...menu} />
|
||||
<MenuItem {...menu} href={changelog()} target="_blank">
|
||||
{t("Changelog")}
|
||||
</MenuItem>
|
||||
<MenuItem {...menu} href={mailToUrl()} target="_blank">
|
||||
{t("Send us feedback")}
|
||||
</MenuItem>
|
||||
<MenuItem {...menu} href={githubIssuesUrl()} target="_blank">
|
||||
{t("Report a bug")}
|
||||
</MenuItem>
|
||||
<Separator {...menu} />
|
||||
<MenuItem {...menu} as={AppearanceMenu} />
|
||||
<Separator {...menu} />
|
||||
<MenuItem {...menu} onClick={auth.logout}>
|
||||
{t("Log out")}
|
||||
</MenuItem>
|
||||
<Template {...menu} items={items} />
|
||||
</ContextMenu>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const ChangeTheme = styled(Flex)`
|
||||
width: 100%;
|
||||
const Logo = styled("img")`
|
||||
border-radius: 2px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
`;
|
||||
|
||||
export default observer(AccountMenu);
|
||||
|
||||
+53
-40
@@ -1,5 +1,13 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import {
|
||||
NewDocumentIcon,
|
||||
EditIcon,
|
||||
TrashIcon,
|
||||
ImportIcon,
|
||||
ExportIcon,
|
||||
PadlockIcon,
|
||||
} from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
@@ -12,7 +20,7 @@ import CollectionExport from "scenes/CollectionExport";
|
||||
import CollectionPermissions from "scenes/CollectionPermissions";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
|
||||
import Template, { filterTemplateItems } from "components/ContextMenu/Template";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
import Modal from "components/Modal";
|
||||
import useStores from "hooks/useStores";
|
||||
import useToasts from "hooks/useToasts";
|
||||
@@ -113,45 +121,50 @@ 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),
|
||||
},
|
||||
]),
|
||||
() => [
|
||||
{
|
||||
title: t("New document"),
|
||||
visible: can.update,
|
||||
onClick: handleNewDocument,
|
||||
icon: <NewDocumentIcon />,
|
||||
},
|
||||
{
|
||||
title: t("Import document"),
|
||||
visible: can.update,
|
||||
onClick: handleImportDocument,
|
||||
icon: <ImportIcon />,
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
title: `${t("Edit")}…`,
|
||||
visible: can.update,
|
||||
onClick: () => setShowCollectionEdit(true),
|
||||
icon: <EditIcon />,
|
||||
},
|
||||
{
|
||||
title: `${t("Permissions")}…`,
|
||||
visible: can.update,
|
||||
onClick: () => setShowCollectionPermissions(true),
|
||||
icon: <PadlockIcon />,
|
||||
},
|
||||
{
|
||||
title: `${t("Export")}…`,
|
||||
visible: !!(collection && can.export),
|
||||
onClick: () => setShowCollectionExport(true),
|
||||
icon: <ExportIcon />,
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
title: `${t("Delete")}…`,
|
||||
visible: !!(collection && can.delete),
|
||||
onClick: () => setShowCollectionDelete(true),
|
||||
icon: <TrashIcon />,
|
||||
},
|
||||
],
|
||||
[can, collection, handleNewDocument, handleImportDocument, t]
|
||||
);
|
||||
|
||||
|
||||
+80
-31
@@ -1,5 +1,25 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import {
|
||||
EditIcon,
|
||||
PinIcon,
|
||||
StarredIcon,
|
||||
UnstarredIcon,
|
||||
DuplicateIcon,
|
||||
ArchiveIcon,
|
||||
TrashIcon,
|
||||
MoveIcon,
|
||||
HistoryIcon,
|
||||
UnpublishIcon,
|
||||
ShapesIcon,
|
||||
PrintIcon,
|
||||
ImportIcon,
|
||||
NewDocumentIcon,
|
||||
DownloadIcon,
|
||||
BuildingBlocksIcon,
|
||||
RestoreIcon,
|
||||
CrossIcon,
|
||||
} from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
@@ -139,6 +159,27 @@ function DocumentMenu({
|
||||
const collection = collections.get(document.collectionId);
|
||||
const can = policies.abilities(document.id);
|
||||
const canViewHistory = can.read && !can.restore;
|
||||
const restoreItems = React.useMemo(
|
||||
() => [
|
||||
...collections.orderedData.reduce((filtered, collection) => {
|
||||
const can = policies.abilities(collection.id);
|
||||
|
||||
if (can.update) {
|
||||
filtered.push({
|
||||
onClick: (ev) => handleRestore(ev, { collectionId: collection.id }),
|
||||
title: (
|
||||
<Flex align="center">
|
||||
<CollectionIcon collection={collection} />
|
||||
<CollectionName>{collection.name}</CollectionName>
|
||||
</Flex>
|
||||
),
|
||||
});
|
||||
}
|
||||
return filtered;
|
||||
}, []),
|
||||
],
|
||||
[collections.orderedData, handleRestore, policies]
|
||||
);
|
||||
|
||||
const stopPropagation = React.useCallback((ev: SyntheticEvent<>) => {
|
||||
ev.stopPropagation();
|
||||
@@ -227,122 +268,127 @@ function DocumentMenu({
|
||||
title: t("Restore"),
|
||||
visible: (!!collection && can.restore) || can.unarchive,
|
||||
onClick: handleRestore,
|
||||
icon: <RestoreIcon />,
|
||||
},
|
||||
{
|
||||
title: t("Restore"),
|
||||
visible: !collection && !!can.restore,
|
||||
visible:
|
||||
!collection && !!can.restore && restoreItems.length !== 0,
|
||||
style: {
|
||||
left: -170,
|
||||
position: "relative",
|
||||
top: -40,
|
||||
},
|
||||
icon: <RestoreIcon />,
|
||||
hover: true,
|
||||
items: [
|
||||
{
|
||||
type: "heading",
|
||||
title: t("Choose a collection"),
|
||||
},
|
||||
...collections.orderedData.map((collection) => {
|
||||
const can = policies.abilities(collection.id);
|
||||
|
||||
return {
|
||||
title: (
|
||||
<Flex align="center">
|
||||
<CollectionIcon collection={collection} />
|
||||
<CollectionName>{collection.name}</CollectionName>
|
||||
</Flex>
|
||||
),
|
||||
onClick: (ev) =>
|
||||
handleRestore(ev, { collectionId: collection.id }),
|
||||
disabled: !can.update,
|
||||
};
|
||||
}),
|
||||
...restoreItems,
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("Unpin"),
|
||||
onClick: document.unpin,
|
||||
visible: !!(showPin && document.pinned && can.unpin),
|
||||
icon: <PinIcon />,
|
||||
},
|
||||
{
|
||||
title: t("Pin to collection"),
|
||||
onClick: document.pin,
|
||||
visible: !!(showPin && !document.pinned && can.pin),
|
||||
icon: <PinIcon />,
|
||||
},
|
||||
{
|
||||
title: t("Unstar"),
|
||||
onClick: handleUnstar,
|
||||
visible: document.isStarred && !!can.unstar,
|
||||
icon: <UnstarredIcon />,
|
||||
},
|
||||
{
|
||||
title: t("Star"),
|
||||
onClick: handleStar,
|
||||
visible: !document.isStarred && !!can.star,
|
||||
},
|
||||
{
|
||||
title: t("Enable embeds"),
|
||||
onClick: document.enableEmbeds,
|
||||
visible: !!showToggleEmbeds && document.embedsDisabled,
|
||||
},
|
||||
{
|
||||
title: t("Disable embeds"),
|
||||
onClick: document.disableEmbeds,
|
||||
visible: !!showToggleEmbeds && !document.embedsDisabled,
|
||||
icon: <StarredIcon />,
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
title: t("Edit"),
|
||||
to: editDocumentUrl(document),
|
||||
visible: !!can.update,
|
||||
icon: <EditIcon />,
|
||||
},
|
||||
{
|
||||
title: t("New nested document"),
|
||||
to: newDocumentUrl(document.collectionId, {
|
||||
parentDocumentId: document.id,
|
||||
}),
|
||||
visible: !!can.createChildDocument,
|
||||
icon: <NewDocumentIcon />,
|
||||
},
|
||||
{
|
||||
title: t("Import document"),
|
||||
visible: can.createChildDocument,
|
||||
onClick: handleImportDocument,
|
||||
icon: <ImportIcon />,
|
||||
},
|
||||
{
|
||||
title: `${t("Create template")}…`,
|
||||
onClick: () => setShowTemplateModal(true),
|
||||
visible: !!can.update && !document.isTemplate,
|
||||
},
|
||||
{
|
||||
title: t("Edit"),
|
||||
to: editDocumentUrl(document),
|
||||
visible: !!can.update,
|
||||
icon: <ShapesIcon />,
|
||||
},
|
||||
{
|
||||
title: t("Duplicate"),
|
||||
onClick: handleDuplicate,
|
||||
visible: !!can.update,
|
||||
icon: <DuplicateIcon />,
|
||||
},
|
||||
{
|
||||
title: t("Unpublish"),
|
||||
onClick: handleUnpublish,
|
||||
visible: !!can.unpublish,
|
||||
icon: <UnpublishIcon />,
|
||||
},
|
||||
{
|
||||
title: t("Archive"),
|
||||
onClick: handleArchive,
|
||||
visible: !!can.archive,
|
||||
icon: <ArchiveIcon />,
|
||||
},
|
||||
{
|
||||
title: `${t("Delete")}…`,
|
||||
onClick: () => setShowDeleteModal(true),
|
||||
visible: !!can.delete,
|
||||
icon: <TrashIcon />,
|
||||
},
|
||||
{
|
||||
title: `${t("Permanently delete")}…`,
|
||||
onClick: () => setShowPermanentDeleteModal(true),
|
||||
visible: can.permanentDelete,
|
||||
icon: <CrossIcon />,
|
||||
},
|
||||
{
|
||||
title: `${t("Move")}…`,
|
||||
onClick: () => setShowMoveModal(true),
|
||||
visible: !!can.move,
|
||||
icon: <MoveIcon />,
|
||||
},
|
||||
{
|
||||
title: t("Enable embeds"),
|
||||
onClick: document.enableEmbeds,
|
||||
visible: !!showToggleEmbeds && document.embedsDisabled,
|
||||
icon: <BuildingBlocksIcon />,
|
||||
},
|
||||
{
|
||||
title: t("Disable embeds"),
|
||||
onClick: document.disableEmbeds,
|
||||
visible: !!showToggleEmbeds && !document.embedsDisabled,
|
||||
icon: <BuildingBlocksIcon />,
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
@@ -353,16 +399,19 @@ function DocumentMenu({
|
||||
? documentUrl(document)
|
||||
: documentHistoryUrl(document),
|
||||
visible: canViewHistory,
|
||||
icon: <HistoryIcon />,
|
||||
},
|
||||
{
|
||||
title: t("Download"),
|
||||
onClick: document.download,
|
||||
visible: !!can.download,
|
||||
icon: <DownloadIcon />,
|
||||
},
|
||||
{
|
||||
title: t("Print"),
|
||||
onClick: handlePrint,
|
||||
visible: !!showPrint,
|
||||
icon: <PrintIcon />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -11,7 +11,6 @@ import CollectionIcon from "components/CollectionIcon";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import Header from "components/ContextMenu/Header";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
import Flex from "components/Flex";
|
||||
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||
import useStores from "hooks/useStores";
|
||||
import { newDocumentUrl } from "utils/routeHelpers";
|
||||
@@ -21,20 +20,32 @@ function NewDocumentMenu() {
|
||||
const { t } = useTranslation();
|
||||
const team = useCurrentTeam();
|
||||
const { collections, policies } = useStores();
|
||||
const singleCollection = collections.orderedData.length === 1;
|
||||
const can = policies.abilities(team.id);
|
||||
|
||||
if (!can.createDocument) {
|
||||
const items = React.useMemo(
|
||||
() =>
|
||||
collections.orderedData.reduce((filtered, collection) => {
|
||||
const can = policies.abilities(collection.id);
|
||||
|
||||
if (can.update) {
|
||||
filtered.push({
|
||||
to: newDocumentUrl(collection.id),
|
||||
title: <CollectionName>{collection.name}</CollectionName>,
|
||||
icon: <CollectionIcon collection={collection} />,
|
||||
});
|
||||
}
|
||||
return filtered;
|
||||
}, []),
|
||||
[collections.orderedData, policies]
|
||||
);
|
||||
|
||||
if (!can.createDocument || items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (singleCollection) {
|
||||
if (items.length === 1) {
|
||||
return (
|
||||
<Button
|
||||
as={Link}
|
||||
to={newDocumentUrl(collections.orderedData[0].id)}
|
||||
icon={<PlusIcon />}
|
||||
>
|
||||
<Button as={Link} to={items[0].to} icon={<PlusIcon />}>
|
||||
{t("New doc")}
|
||||
</Button>
|
||||
);
|
||||
@@ -51,19 +62,7 @@ function NewDocumentMenu() {
|
||||
</MenuButton>
|
||||
<ContextMenu {...menu} aria-label={t("New document")}>
|
||||
<Header>{t("Choose a collection")}</Header>
|
||||
<Template
|
||||
{...menu}
|
||||
items={collections.orderedData.map((collection) => ({
|
||||
to: newDocumentUrl(collection.id),
|
||||
disabled: !policies.abilities(collection.id).update,
|
||||
title: (
|
||||
<Flex align="center">
|
||||
<CollectionIcon collection={collection} />
|
||||
<CollectionName>{collection.name}</CollectionName>
|
||||
</Flex>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
<Template {...menu} items={items} />
|
||||
</ContextMenu>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -3,14 +3,13 @@ import { observer } from "mobx-react";
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMenuState, MenuButton } from "reakit/Menu";
|
||||
import { MenuButton, useMenuState } from "reakit/Menu";
|
||||
import styled from "styled-components";
|
||||
import Button from "components/Button";
|
||||
import CollectionIcon from "components/CollectionIcon";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import Header from "components/ContextMenu/Header";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
import Flex from "components/Flex";
|
||||
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||
import useStores from "hooks/useStores";
|
||||
import { newDocumentUrl } from "utils/routeHelpers";
|
||||
@@ -22,7 +21,23 @@ function NewTemplateMenu() {
|
||||
const { collections, policies } = useStores();
|
||||
const can = policies.abilities(team.id);
|
||||
|
||||
if (!can.createDocument) {
|
||||
const items = React.useMemo(
|
||||
() =>
|
||||
collections.orderedData.reduce((filtered, collection) => {
|
||||
const can = policies.abilities(collection.id);
|
||||
if (can.update) {
|
||||
filtered.push({
|
||||
to: newDocumentUrl(collection.id, { template: true }),
|
||||
title: <CollectionName>{collection.name}</CollectionName>,
|
||||
icon: <CollectionIcon collection={collection} />,
|
||||
});
|
||||
}
|
||||
return filtered;
|
||||
}, []),
|
||||
[collections.orderedData, policies]
|
||||
);
|
||||
|
||||
if (!can.createDocument || items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -37,21 +52,7 @@ function NewTemplateMenu() {
|
||||
</MenuButton>
|
||||
<ContextMenu aria-label={t("New template")} {...menu}>
|
||||
<Header>{t("Choose a collection")}</Header>
|
||||
<Template
|
||||
{...menu}
|
||||
items={collections.orderedData.map((collection) => ({
|
||||
to: newDocumentUrl(collection.id, {
|
||||
template: true,
|
||||
}),
|
||||
disabled: !policies.abilities(collection.id).update,
|
||||
title: (
|
||||
<Flex align="center">
|
||||
<CollectionIcon collection={collection} />
|
||||
<CollectionName>{collection.name}</CollectionName>
|
||||
</Flex>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
<Template {...menu} items={items} />
|
||||
</ContextMenu>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { RestoreIcon, LinkIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { useMenuState } from "reakit/Menu";
|
||||
import Document from "models/Document";
|
||||
import Revision from "models/Revision";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import MenuItem from "components/ContextMenu/MenuItem";
|
||||
import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
|
||||
import Separator from "components/ContextMenu/Separator";
|
||||
import CopyToClipboard from "components/CopyToClipboard";
|
||||
import MenuIconWrapper from "components/MenuIconWrapper";
|
||||
import useToasts from "hooks/useToasts";
|
||||
import { documentHistoryUrl } from "utils/routeHelpers";
|
||||
|
||||
type Props = {|
|
||||
document: Document,
|
||||
revision: Revision,
|
||||
iconColor?: string,
|
||||
revisionId: string,
|
||||
className?: string,
|
||||
|};
|
||||
|
||||
function RevisionMenu({ document, revision, className, iconColor }: Props) {
|
||||
function RevisionMenu({ document, revisionId, className }: Props) {
|
||||
const { showToast } = useToasts();
|
||||
const menu = useMenuState({ modal: true });
|
||||
const { t } = useTranslation();
|
||||
@@ -30,11 +30,11 @@ function RevisionMenu({ document, revision, className, iconColor }: Props) {
|
||||
const handleRestore = React.useCallback(
|
||||
async (ev: SyntheticEvent<>) => {
|
||||
ev.preventDefault();
|
||||
await document.restore({ revisionId: revision.id });
|
||||
await document.restore({ revisionId });
|
||||
showToast(t("Document restored"), { type: "success" });
|
||||
history.push(document.url);
|
||||
},
|
||||
[history, showToast, t, document, revision]
|
||||
[history, showToast, t, document, revisionId]
|
||||
);
|
||||
|
||||
const handleCopy = React.useCallback(() => {
|
||||
@@ -43,24 +43,32 @@ function RevisionMenu({ document, revision, className, iconColor }: Props) {
|
||||
|
||||
const url = `${window.location.origin}${documentHistoryUrl(
|
||||
document,
|
||||
revision.id
|
||||
revisionId
|
||||
)}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<OverflowMenuButton
|
||||
className={className}
|
||||
iconColor={iconColor}
|
||||
iconColor="currentColor"
|
||||
aria-label={t("Show menu")}
|
||||
{...menu}
|
||||
/>
|
||||
<ContextMenu {...menu} aria-label={t("Revision options")}>
|
||||
<MenuItem {...menu} onClick={handleRestore}>
|
||||
<MenuIconWrapper>
|
||||
<RestoreIcon />
|
||||
</MenuIconWrapper>
|
||||
{t("Restore version")}
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<CopyToClipboard text={url} onCopy={handleCopy}>
|
||||
<MenuItem {...menu}>{t("Copy link")}</MenuItem>
|
||||
<MenuItem {...menu}>
|
||||
<MenuIconWrapper>
|
||||
<LinkIcon />
|
||||
</MenuIconWrapper>
|
||||
{t("Copy link")}
|
||||
</MenuItem>
|
||||
</CopyToClipboard>
|
||||
</ContextMenu>
|
||||
</>
|
||||
|
||||
@@ -37,9 +37,9 @@ function TemplatesMenu({ document }: Props) {
|
||||
<MenuItem
|
||||
key={template.id}
|
||||
onClick={() => document.updateFromTemplate(template)}
|
||||
icon={<DocumentIcon />}
|
||||
{...menu}
|
||||
>
|
||||
<DocumentIcon />
|
||||
<TemplateItem>
|
||||
<strong>{template.titleWithDefault}</strong>
|
||||
<br />
|
||||
|
||||
@@ -20,20 +20,6 @@ class Event extends BaseModel {
|
||||
published: boolean,
|
||||
templateId: string,
|
||||
};
|
||||
|
||||
get model() {
|
||||
return this.name.split(".")[0];
|
||||
}
|
||||
|
||||
get verb() {
|
||||
return this.name.split(".")[1];
|
||||
}
|
||||
|
||||
get verbPastTense() {
|
||||
const v = this.verb;
|
||||
if (v.endsWith("e")) return `${v}d`;
|
||||
return `${v}ed`;
|
||||
}
|
||||
}
|
||||
|
||||
export default Event;
|
||||
|
||||
@@ -8,7 +8,6 @@ import Drafts from "scenes/Drafts";
|
||||
import Error404 from "scenes/Error404";
|
||||
import Home from "scenes/Home";
|
||||
import Search from "scenes/Search";
|
||||
import Starred from "scenes/Starred";
|
||||
import Templates from "scenes/Templates";
|
||||
import Trash from "scenes/Trash";
|
||||
|
||||
@@ -51,13 +50,12 @@ export default function AuthenticatedRoutes() {
|
||||
<Redirect from="/dashboard" to="/home" />
|
||||
<Route path="/home/:tab" component={Home} />
|
||||
<Route path="/home" component={Home} />
|
||||
<Route exact path="/starred" component={Starred} />
|
||||
<Route exact path="/starred/:sort" component={Starred} />
|
||||
<Route exact path="/templates" component={Templates} />
|
||||
<Route exact path="/templates/:sort" component={Templates} />
|
||||
<Route exact path="/drafts" component={Drafts} />
|
||||
<Route exact path="/archive" component={Archive} />
|
||||
<Route exact path="/trash" component={Trash} />
|
||||
<Redirect exact from="/starred" to="/home" />
|
||||
<Redirect exact from="/collections/*" to="/collection/*" />
|
||||
<Route exact path="/collection/:id/new" component={DocumentNew} />
|
||||
<Route exact path="/collection/:id/:tab" component={Collection} />
|
||||
|
||||
@@ -79,7 +79,6 @@ function DocumentHeader({
|
||||
const isNew = document.isNewDocument;
|
||||
const isTemplate = document.isTemplate;
|
||||
const can = policies.abilities(document.id);
|
||||
const canShareDocument = auth.team && auth.team.sharing && can.share;
|
||||
const canToggleEmbeds = auth.team && auth.team.documentEmbeds;
|
||||
const canEdit = can.update && !isEditing;
|
||||
|
||||
@@ -171,7 +170,7 @@ function DocumentHeader({
|
||||
<TemplatesMenu document={document} />
|
||||
</Action>
|
||||
)}
|
||||
{!isEditing && canShareDocument && (!isMobile || !isTemplate) && (
|
||||
{!isEditing && (!isMobile || !isTemplate) && (
|
||||
<Action>
|
||||
<ShareButton document={document} />
|
||||
</Action>
|
||||
|
||||
@@ -27,12 +27,17 @@ type Props = {|
|
||||
|
||||
function SharePopover({ document, share, sharedParent, onSubmit }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { policies, shares } = useStores();
|
||||
const { policies, shares, auth } = useStores();
|
||||
const { showToast } = useToasts();
|
||||
const [isCopied, setIsCopied] = React.useState(false);
|
||||
const timeout = React.useRef<?TimeoutID>();
|
||||
const can = policies.abilities(share ? share.id : "");
|
||||
const canPublish = can.update && !document.isTemplate;
|
||||
const documentAbilities = policies.abilities(document.id);
|
||||
const canPublish =
|
||||
can.update &&
|
||||
!document.isTemplate &&
|
||||
auth.team?.sharing &&
|
||||
documentAbilities.share;
|
||||
const isPubliclyShared = (share && share.published) || sharedParent;
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -102,7 +107,7 @@ function SharePopover({ document, share, sharedParent, onSubmit }: Props) {
|
||||
</Notice>
|
||||
)}
|
||||
|
||||
{canPublish && (
|
||||
{canPublish ? (
|
||||
<SwitchWrapper>
|
||||
<Switch
|
||||
id="published"
|
||||
@@ -132,8 +137,11 @@ function SharePopover({ document, share, sharedParent, onSubmit }: Props) {
|
||||
</SwitchText>
|
||||
</SwitchLabel>
|
||||
</SwitchWrapper>
|
||||
) : (
|
||||
<HelpText>{t("Only team members with permission can view")}</HelpText>
|
||||
)}
|
||||
{share && share.published && (
|
||||
|
||||
{canPublish && share?.published && (
|
||||
<SwitchWrapper>
|
||||
<Switch
|
||||
id="includeChildDocuments"
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ function Home() {
|
||||
documents={documents.createdByUser(user)}
|
||||
fetch={documents.fetchOwned}
|
||||
options={{ user }}
|
||||
empty={<Empty>{t("Weird, this shouldn’t ever be empty")}</Empty>}
|
||||
empty={<Empty>{t("You haven’t created any documents yet")}</Empty>}
|
||||
showCollection
|
||||
/>
|
||||
</Route>
|
||||
|
||||
@@ -268,6 +268,7 @@ class Search extends React.Component<Props> {
|
||||
const showShortcutTip =
|
||||
!this.pinToTop && location.state && location.state.fromMenu;
|
||||
const can = policies.abilities(auth.team?.id ? auth.team.id : "");
|
||||
const canCollection = policies.abilities(this.collectionId || "");
|
||||
|
||||
return (
|
||||
<Container auto>
|
||||
@@ -335,7 +336,9 @@ class Search extends React.Component<Props> {
|
||||
{can.createDocument && <Trans>Create a new document?</Trans>}
|
||||
</HelpText>
|
||||
<Wrapper>
|
||||
{this.collectionId && can.createDocument ? (
|
||||
{this.collectionId &&
|
||||
can.createDocument &&
|
||||
canCollection.update ? (
|
||||
<Button
|
||||
onClick={this.handleNewDoc}
|
||||
icon={<PlusIcon />}
|
||||
|
||||
@@ -226,6 +226,7 @@ function People(props) {
|
||||
onChangePage={handleChangePage}
|
||||
page={page}
|
||||
totalPages={totalPages}
|
||||
defaultSortDirection="ASC"
|
||||
/>
|
||||
{can.inviteUser && (
|
||||
<Modal
|
||||
|
||||
@@ -18,38 +18,29 @@ function Security() {
|
||||
const team = useCurrentTeam();
|
||||
const { t } = useTranslation();
|
||||
const { showToast } = useToasts();
|
||||
const [sharing, setSharing] = useState(team.documentEmbeds);
|
||||
const [documentEmbeds, setDocumentEmbeds] = useState(team.guestSignin);
|
||||
const [guestSignin, setGuestSignin] = useState(team.sharing);
|
||||
const [data, setData] = useState({
|
||||
sharing: team.sharing,
|
||||
documentEmbeds: team.documentEmbeds,
|
||||
guestSignin: team.guestSignin,
|
||||
});
|
||||
|
||||
const showSuccessMessage = debounce(() => {
|
||||
showToast(t("Settings saved"), { type: "success" });
|
||||
}, 500);
|
||||
const showSuccessMessage = React.useCallback(
|
||||
debounce(() => {
|
||||
showToast(t("Settings saved"), { type: "success" });
|
||||
}, 250),
|
||||
[t, showToast]
|
||||
);
|
||||
|
||||
const handleChange = React.useCallback(
|
||||
async (ev: SyntheticInputEvent<*>) => {
|
||||
switch (ev.target.name) {
|
||||
case "sharing":
|
||||
setSharing(ev.target.checked);
|
||||
break;
|
||||
case "documentEmbeds":
|
||||
setDocumentEmbeds(ev.target.checked);
|
||||
break;
|
||||
case "guestSignin":
|
||||
setGuestSignin(ev.target.checked);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
const newData = { ...data, [ev.target.name]: ev.target.checked };
|
||||
setData(newData);
|
||||
|
||||
await auth.updateTeam({
|
||||
sharing,
|
||||
documentEmbeds,
|
||||
guestSignin,
|
||||
});
|
||||
await auth.updateTeam(newData);
|
||||
|
||||
showSuccessMessage();
|
||||
},
|
||||
[auth, sharing, documentEmbeds, guestSignin, showSuccessMessage]
|
||||
[auth, data, showSuccessMessage]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -67,14 +58,14 @@ function Security() {
|
||||
<Checkbox
|
||||
label={t("Allow email authentication")}
|
||||
name="guestSignin"
|
||||
checked={guestSignin}
|
||||
checked={data.guestSignin}
|
||||
onChange={handleChange}
|
||||
note={t("When enabled, users can sign-in using their email address")}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t("Public document sharing")}
|
||||
name="sharing"
|
||||
checked={sharing}
|
||||
checked={data.sharing}
|
||||
onChange={handleChange}
|
||||
note={t(
|
||||
"When enabled, documents can be shared publicly on the internet by any team member"
|
||||
@@ -83,7 +74,7 @@ function Security() {
|
||||
<Checkbox
|
||||
label={t("Rich service embeds")}
|
||||
name="documentEmbeds"
|
||||
checked={documentEmbeds}
|
||||
checked={data.documentEmbeds}
|
||||
onChange={handleChange}
|
||||
note={t(
|
||||
"Links to supported services are shown as rich embeds within your documents"
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { StarredIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type Match } from "react-router-dom";
|
||||
import { Action } from "components/Actions";
|
||||
import Empty from "components/Empty";
|
||||
import Heading from "components/Heading";
|
||||
import InputSearchPage from "components/InputSearchPage";
|
||||
import PaginatedDocumentList from "components/PaginatedDocumentList";
|
||||
import Scene from "components/Scene";
|
||||
import Tab from "components/Tab";
|
||||
import Tabs from "components/Tabs";
|
||||
import useStores from "hooks/useStores";
|
||||
import NewDocumentMenu from "menus/NewDocumentMenu";
|
||||
|
||||
type Props = {
|
||||
match: Match,
|
||||
};
|
||||
|
||||
function Starred(props: Props) {
|
||||
const { documents } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const { fetchStarred, starred, starredAlphabetical } = documents;
|
||||
const { sort } = props.match.params;
|
||||
|
||||
return (
|
||||
<Scene
|
||||
icon={<StarredIcon color="currentColor" />}
|
||||
title={t("Starred")}
|
||||
actions={
|
||||
<>
|
||||
<Action>
|
||||
<InputSearchPage source="starred" label={t("Search documents")} />
|
||||
</Action>
|
||||
<Action>
|
||||
<NewDocumentMenu />
|
||||
</Action>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Heading>{t("Starred")}</Heading>
|
||||
<PaginatedDocumentList
|
||||
heading={
|
||||
<Tabs>
|
||||
<Tab to="/starred" exact>
|
||||
{t("Recently updated")}
|
||||
</Tab>
|
||||
<Tab to="/starred/alphabetical" exact>
|
||||
{t("Alphabetical")}
|
||||
</Tab>
|
||||
</Tabs>
|
||||
}
|
||||
empty={<Empty>{t("You’ve not starred any documents yet.")}</Empty>}
|
||||
fetch={fetchStarred}
|
||||
documents={sort === "alphabetical" ? starredAlphabetical : starred}
|
||||
showCollection
|
||||
/>
|
||||
</Scene>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(Starred);
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { TemplateIcon } from "outline-icons";
|
||||
import { ShapesIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type Match } from "react-router-dom";
|
||||
@@ -29,7 +29,7 @@ function Templates(props: Props) {
|
||||
|
||||
return (
|
||||
<Scene
|
||||
icon={<TemplateIcon color="currentColor" />}
|
||||
icon={<ShapesIcon color="currentColor" />}
|
||||
title={t("Templates")}
|
||||
actions={
|
||||
<Action>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// @flow
|
||||
import { sortBy, filter } from "lodash";
|
||||
import { computed } from "mobx";
|
||||
import Event from "models/Event";
|
||||
import BaseStore from "./BaseStore";
|
||||
import RootStore from "./RootStore";
|
||||
|
||||
export default class EventsStore extends BaseStore<Event> {
|
||||
actions = ["list"];
|
||||
|
||||
constructor(rootStore: RootStore) {
|
||||
super(rootStore, Event);
|
||||
}
|
||||
|
||||
@computed
|
||||
get orderedData(): Event[] {
|
||||
return sortBy(Array.from(this.data.values()), "createdAt").reverse();
|
||||
}
|
||||
|
||||
inDocument(documentId: string): Event[] {
|
||||
return filter(this.orderedData, (event) => event.documentId === documentId);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import CollectionGroupMembershipsStore from "./CollectionGroupMembershipsStore";
|
||||
import CollectionsStore from "./CollectionsStore";
|
||||
import DocumentPresenceStore from "./DocumentPresenceStore";
|
||||
import DocumentsStore from "./DocumentsStore";
|
||||
import EventsStore from "./EventsStore";
|
||||
import GroupMembershipsStore from "./GroupMembershipsStore";
|
||||
import GroupsStore from "./GroupsStore";
|
||||
import IntegrationsStore from "./IntegrationsStore";
|
||||
@@ -24,6 +25,7 @@ export default class RootStore {
|
||||
collections: CollectionsStore;
|
||||
collectionGroupMemberships: CollectionGroupMembershipsStore;
|
||||
documents: DocumentsStore;
|
||||
events: EventsStore;
|
||||
groups: GroupsStore;
|
||||
groupMemberships: GroupMembershipsStore;
|
||||
integrations: IntegrationsStore;
|
||||
@@ -46,6 +48,7 @@ export default class RootStore {
|
||||
this.collections = new CollectionsStore(this);
|
||||
this.collectionGroupMemberships = new CollectionGroupMembershipsStore(this);
|
||||
this.documents = new DocumentsStore(this);
|
||||
this.events = new EventsStore(this);
|
||||
this.groups = new GroupsStore(this);
|
||||
this.groupMemberships = new GroupMembershipsStore(this);
|
||||
this.integrations = new IntegrationsStore(this);
|
||||
@@ -66,6 +69,7 @@ export default class RootStore {
|
||||
this.collections.clear();
|
||||
this.collectionGroupMemberships.clear();
|
||||
this.documents.clear();
|
||||
this.events.clear();
|
||||
this.groups.clear();
|
||||
this.groupMemberships.clear();
|
||||
this.integrations.clear();
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
import localStorage from '../../__mocks__/localStorage';
|
||||
import Enzyme from "enzyme";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
import { initI18n } from "shared/i18n";
|
||||
|
||||
initI18n();
|
||||
|
||||
Enzyme.configure({ adapter: new Adapter() });
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ export type MenuItem =
|
||||
visible?: boolean,
|
||||
selected?: boolean,
|
||||
disabled?: boolean,
|
||||
icon?: React.Node,
|
||||
|}
|
||||
| {|
|
||||
title: React.Node,
|
||||
@@ -73,6 +74,7 @@ export type MenuItem =
|
||||
visible?: boolean,
|
||||
selected?: boolean,
|
||||
disabled?: boolean,
|
||||
icon?: React.Node,
|
||||
|}
|
||||
| {|
|
||||
title: React.Node,
|
||||
@@ -81,6 +83,7 @@ export type MenuItem =
|
||||
selected?: boolean,
|
||||
disabled?: boolean,
|
||||
level?: number,
|
||||
icon?: React.Node,
|
||||
|}
|
||||
| {|
|
||||
title: React.Node,
|
||||
@@ -89,6 +92,7 @@ export type MenuItem =
|
||||
style?: Object,
|
||||
hover?: boolean,
|
||||
items: MenuItem[],
|
||||
icon?: React.Node,
|
||||
|}
|
||||
| {|
|
||||
type: "separator",
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// @flow
|
||||
import {
|
||||
isToday,
|
||||
isYesterday,
|
||||
differenceInCalendarWeeks,
|
||||
differenceInCalendarMonths,
|
||||
differenceInCalendarYears,
|
||||
format as formatDate,
|
||||
} from "date-fns";
|
||||
import { type TFunction } from "react-i18next";
|
||||
import { dateLocale } from "utils/i18n";
|
||||
|
||||
export function dateToHeading(
|
||||
dateTime: string,
|
||||
t: TFunction,
|
||||
userLocale: ?string
|
||||
) {
|
||||
const date = Date.parse(dateTime);
|
||||
const now = new Date();
|
||||
const locale = dateLocale(userLocale);
|
||||
|
||||
if (isToday(date)) {
|
||||
return t("Today");
|
||||
}
|
||||
|
||||
if (isYesterday(date)) {
|
||||
return t("Yesterday");
|
||||
}
|
||||
|
||||
// If the current calendar week but not today or yesterday then return the day
|
||||
// of the week as a string. We use the LocaleTime component here to gain
|
||||
// async bundle loading of languages
|
||||
const weekDiff = differenceInCalendarWeeks(now, date);
|
||||
if (weekDiff === 0) {
|
||||
return formatDate(Date.parse(dateTime), "iiii", { locale });
|
||||
}
|
||||
|
||||
if (weekDiff === 1) {
|
||||
return t("Last week");
|
||||
}
|
||||
|
||||
const monthDiff = differenceInCalendarMonths(now, date);
|
||||
if (monthDiff === 0) {
|
||||
return t("This month");
|
||||
}
|
||||
|
||||
if (monthDiff === 1) {
|
||||
return t("Last month");
|
||||
}
|
||||
|
||||
const yearDiff = differenceInCalendarYears(now, date);
|
||||
if (yearDiff === 0) {
|
||||
return t("This year");
|
||||
}
|
||||
|
||||
// If older than the current calendar year then just print the year e.g 2020
|
||||
return formatDate(Date.parse(dateTime), "y", { locale });
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// @flow
|
||||
import {
|
||||
enUS,
|
||||
de,
|
||||
faIR,
|
||||
fr,
|
||||
es,
|
||||
it,
|
||||
ja,
|
||||
ko,
|
||||
ptBR,
|
||||
pt,
|
||||
zhCN,
|
||||
zhTW,
|
||||
ru,
|
||||
} from "date-fns/locale";
|
||||
|
||||
const locales = {
|
||||
en_US: enUS,
|
||||
de_DE: de,
|
||||
es_ES: es,
|
||||
fa_IR: faIR,
|
||||
fr_FR: fr,
|
||||
it_IT: it,
|
||||
ja_JP: ja,
|
||||
ko_KR: ko,
|
||||
pt_BR: ptBR,
|
||||
pt_PT: pt,
|
||||
zh_CN: zhCN,
|
||||
zh_TW: zhTW,
|
||||
ru_RU: ru,
|
||||
};
|
||||
|
||||
export function dateLocale(userLocale: ?string) {
|
||||
return userLocale ? locales[userLocale] : undefined;
|
||||
}
|
||||
@@ -7,10 +7,6 @@ export function homeUrl(): string {
|
||||
return "/home";
|
||||
}
|
||||
|
||||
export function starredUrl(): string {
|
||||
return "/starred";
|
||||
}
|
||||
|
||||
export function newCollectionUrl(): string {
|
||||
return "/collection/new";
|
||||
}
|
||||
|
||||
+2
-2
@@ -143,7 +143,7 @@
|
||||
"react-window": "^1.8.6",
|
||||
"reakit": "^1.3.8",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"rich-markdown-editor": "^11.17.0",
|
||||
"rich-markdown-editor": "^11.17.2",
|
||||
"semver": "^7.3.2",
|
||||
"sequelize": "^6.3.4",
|
||||
"sequelize-cli": "^6.2.0",
|
||||
@@ -211,5 +211,5 @@
|
||||
"dot-prop": "^5.2.0",
|
||||
"js-yaml": "^3.13.1"
|
||||
},
|
||||
"version": "0.57.0"
|
||||
"version": "0.58.0"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
+25
-6
@@ -36,7 +36,7 @@ import { sequelize } from "../sequelize";
|
||||
import pagination from "./middlewares/pagination";
|
||||
|
||||
const Op = Sequelize.Op;
|
||||
const { authorize, cannot } = policy;
|
||||
const { authorize, cannot, can } = policy;
|
||||
const router = new Router();
|
||||
|
||||
router.post("documents.list", auth(), pagination(), async (ctx) => {
|
||||
@@ -310,9 +310,10 @@ router.post("documents.viewed", auth(), pagination(), async (ctx) => {
|
||||
|
||||
const user = ctx.state.user;
|
||||
const collectionIds = await user.collectionIds();
|
||||
const userId = user.id;
|
||||
|
||||
const views = await View.findAll({
|
||||
where: { userId: user.id },
|
||||
where: { userId },
|
||||
order: [[sort, direction]],
|
||||
include: [
|
||||
{
|
||||
@@ -325,9 +326,16 @@ router.post("documents.viewed", auth(), pagination(), async (ctx) => {
|
||||
{
|
||||
model: Star,
|
||||
as: "starred",
|
||||
where: { userId: user.id },
|
||||
where: { userId },
|
||||
separate: true,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
model: Collection.scope({
|
||||
method: ["withMembership", userId],
|
||||
}),
|
||||
as: "collection",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -526,10 +534,21 @@ async function loadDocument({
|
||||
document = share.document;
|
||||
}
|
||||
|
||||
// "published" === on the public internet. So if the share isn't published
|
||||
// then we must have permission to read the document
|
||||
// If the user has access to read the document, we can just update
|
||||
// the last access date and return the document without additional checks.
|
||||
document.fromShare = true;
|
||||
const canReadDocument = can(user, "read", document);
|
||||
if (canReadDocument) {
|
||||
await share.update({ lastAccessedAt: new Date() });
|
||||
|
||||
return { document, share, collection };
|
||||
}
|
||||
|
||||
// "published" === on the public internet.
|
||||
// We already know that there's either no logged in user or the user doesn't
|
||||
// have permission to read the document, so we can throw an error.
|
||||
if (!share.published) {
|
||||
authorize(user, "read", document);
|
||||
throw new AuthorizationError();
|
||||
}
|
||||
|
||||
// It is possible to disable sharing at the collection so we must check
|
||||
|
||||
@@ -235,6 +235,27 @@ describe("#documents.info", () => {
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
|
||||
it("should return document from shareId if public sharing is disabled but the user has permission to read", async () => {
|
||||
const { document, collection, team, user } = await seed();
|
||||
const share = await buildShare({
|
||||
documentId: document.id,
|
||||
teamId: document.teamId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
team.sharing = false;
|
||||
await team.save();
|
||||
|
||||
collection.sharing = false;
|
||||
await collection.save();
|
||||
|
||||
const res = await server.post("/api/documents.info", {
|
||||
body: { token: user.getJwtToken(), shareId: share.id },
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
});
|
||||
|
||||
it("should not return document from revoked shareId", async () => {
|
||||
const { document, user } = await seed();
|
||||
const share = await buildShare({
|
||||
@@ -1349,6 +1370,7 @@ describe("#documents.viewed", () => {
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(1);
|
||||
expect(body.data[0].id).toEqual(document.id);
|
||||
expect(body.policies[0].abilities.update).toEqual(true);
|
||||
});
|
||||
|
||||
it("should not return recently viewed but deleted documents", async () => {
|
||||
@@ -1918,12 +1940,16 @@ describe("#documents.update", () => {
|
||||
|
||||
it("should not add template to collection structure when publishing", async () => {
|
||||
const user = await buildUser();
|
||||
const collection = await buildCollection({ teamId: user.teamId });
|
||||
const collection = await buildCollection({
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
});
|
||||
const template = await buildDocument({
|
||||
teamId: user.teamId,
|
||||
collectionId: collection.id,
|
||||
template: true,
|
||||
publishedAt: null,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/documents.update", {
|
||||
|
||||
+2
-2
@@ -71,13 +71,13 @@ router.post("users.list", auth(), pagination(), async (ctx) => {
|
||||
}
|
||||
|
||||
const [users, total] = await Promise.all([
|
||||
await User.findAll({
|
||||
User.findAll({
|
||||
where,
|
||||
order: [[sort, direction]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
}),
|
||||
await User.count({
|
||||
User.count({
|
||||
where,
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -45,6 +45,7 @@ router.get("/redirect", auth(), async (ctx) => {
|
||||
Team.findByPk(user.teamId),
|
||||
Collection.findOne({
|
||||
where: { teamId: user.teamId },
|
||||
order: [["index", "ASC"]],
|
||||
}),
|
||||
View.findOne({
|
||||
where: { userId: user.id },
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeIndex("documents", "documents_collaborator_ids");
|
||||
await queryInterface.removeIndex("documents", "documents_id_deleted_at");
|
||||
await queryInterface.removeIndex("users", "users_slack_id");
|
||||
await queryInterface.removeIndex("teams", "teams_slack_id");
|
||||
await queryInterface.removeIndex("teams", "teams_google_id");
|
||||
await queryInterface.removeIndex("collection_users", "collection_users_permission");
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addIndex("documents", ["collaboratorIds"]);
|
||||
await queryInterface.addIndex("documents", ["id", "deletedAt"]);
|
||||
await queryInterface.addIndex("users", ["slackId"]);
|
||||
await queryInterface.addIndex("teams", ["slackId"]);
|
||||
await queryInterface.addIndex("teams", ["googleId"]);
|
||||
await queryInterface.addIndex("collection_users", ["permission"]);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeColumn('documents', 'backup');
|
||||
await queryInterface.removeColumn('revisions', 'backup');
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn('documents', 'backup', {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('revisions', 'backup', {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -192,13 +192,25 @@ Document.associate = (models) => {
|
||||
|
||||
return {
|
||||
include: [
|
||||
{ model: models.View, as: "views", where: { userId }, required: false },
|
||||
{
|
||||
model: models.View,
|
||||
as: "views",
|
||||
where: { userId },
|
||||
required: false,
|
||||
separate: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
Document.addScope("withStarred", (userId) => ({
|
||||
include: [
|
||||
{ model: models.Star, as: "starred", where: { userId }, required: false },
|
||||
{
|
||||
model: models.Star,
|
||||
as: "starred",
|
||||
where: { userId },
|
||||
required: false,
|
||||
separate: true,
|
||||
},
|
||||
],
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -170,9 +170,9 @@ Team.prototype.provisionFirstCollection = async function (userId) {
|
||||
// For the first collection we go ahead and create some intitial documents to get
|
||||
// the team started. You can edit these in /server/onboarding/x.md
|
||||
const onboardingDocs = [
|
||||
"Support",
|
||||
"Integrations & API",
|
||||
"Our Editor",
|
||||
"Getting Started",
|
||||
"What is Outline",
|
||||
];
|
||||
|
||||
@@ -182,7 +182,7 @@ Team.prototype.provisionFirstCollection = async function (userId) {
|
||||
"utf8"
|
||||
);
|
||||
const document = await Document.create({
|
||||
version: 1,
|
||||
version: 2,
|
||||
isWelcome: true,
|
||||
parentDocumentId: null,
|
||||
collectionId: collection.id,
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
Some ideas to get you and your team started with learning the basics of Outline, feel free to check them off as you go!
|
||||
|
||||
## Learn the basics
|
||||
|
||||
- [x] Create an Outline account
|
||||
- [ ] **Create a collection** from the left sidebar
|
||||
- [ ] **Create a new doc** from the top right of home or any collection
|
||||
- [ ] Try drag and drop to nest and move documents
|
||||
- [ ] Share a document
|
||||
- [ ] Invite a co-worker 👋
|
||||
|
||||
## More to try
|
||||
|
||||
- [ ] Setup the [Slack integration](/settings/integrations/slack)
|
||||
- [ ] **Create a template** to share a writing structure with your team
|
||||
- [ ] Create a check list to track tasks
|
||||
- [ ] Try embedding a supported [integration](https://www.getoutline.com/integrations)
|
||||
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
Outline supports many of the most popular tools on the market without any additional settings or configuration. Just paste links to a YouTube video, Figma file, or Google Spreadsheet to get instant live-embeds in your documents. Take a look at the [integrations directory](https://www.getoutline.com/integrations) for a list of all of the tools that are supported.
|
||||
|
||||
\
|
||||
Our integration code is also [open-source](https://github.com/outline/outline) and we encourage third party developers and the community to build support for additional tools!
|
||||
|
||||
\
|
||||
:::info
|
||||
Most integrations work by simply pasting a link from a supported service into a document.
|
||||
:::
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
The heart of Outline is the document editor. We let you write in whichever way you prefer – be it Markdown, WYSIWYG, or taking advantage of the many keyboard shortcuts.
|
||||
The heart of Outline is the document editor. We let you write in whichever way you prefer – be it Markdown, WYSIWYG, or taking advantage of the many keyboard shortcuts (Type `?` to see them all).
|
||||
|
||||
## Markdown
|
||||
|
||||
If you’re comfortable writing markdown then all of the shortcuts you are used to are supported, for example type \*\*bold\*\* to instantly create bold text. If you forget some syntax or are after a quick refresher hit the keyboard icon in the bottom right hand corner for our guide. Learning some of the key Markdown shortcuts will make using Outline faster and more enjoyable!
|
||||
|
||||

|
||||
If you’re comfortable writing markdown then all of the shortcuts you are used to are supported, for example type \*\*bold\*\* to instantly create **bold** text. If you forget some syntax or are after a quick refresher hit the keyboard icon in the bottom right hand corner for our guide. Learning some of the key Markdown shortcuts will definitely make using Outline faster and more enjoyable!
|
||||
|
||||
\
|
||||
:::info
|
||||
You can also paste markdown or html from elsewhere directly into a document.
|
||||
You can also paste markdown, html, or rich text from elsewhere directly into a document.
|
||||
:::
|
||||
|
||||
|
||||
## Rich documents
|
||||
|
||||
The editor supports a variety of content blocks including images, tables, lists, quotes, videos, and more. Type "/" on an empty line or click on the "+" icon to trigger the block insert menu, you can keep typing to filter it down.
|
||||
|
||||
You can also drag and drop images to include them in your document or paste a link to embed content from one of the many supported [integrations](https://www.getoutline.com/integrations)
|
||||
|
||||

|
||||
|
||||
## References
|
||||
|
||||
Linking to another document automatically creates backlinks which are kept up-to-date and shown at the bottom of the document, so you can create a library of linked information and easily answer the question "which other documents link here?".
|
||||
Linking to another document automatically creates backlinks which are kept up-to-date and shown at the bottom of the document, so you can create a library of linked information and easily answer the question "which other documents link here?".
|
||||
|
||||
## Rich documents
|
||||
|
||||
The editor supports a variety of content blocks including images, tables, lists, quotes, task lists, videos, and more. Type `/` on an empty line or click on the `+` icon to trigger the block insert menu, you can keep typing to filter it down. Of course, you can also drag and drop images to include them in your document or paste a link to embed content from one of the many supported [integrations](https://www.getoutline.com/integrations)
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
We hate bugs as much as you and do everything possible to keep the application bug-free and performant. If you see any problems with Outline please get in touch with the team – you can email [hello@getoutline.com](mailto:hello@getoutline.com) directly and we’ll get back to you (hopefully with a fix!) as soon as possible.
|
||||
|
||||
## GitHub
|
||||
|
||||
If you have a GitHub account then you can also submit bugs directly to the development team on our public [issue tracker](https://github.com/outline/outline/issues).
|
||||
|
||||
## Ideas
|
||||
|
||||
We’d love to hear your ideas about how Outline can be improved and features you would like to see built. The best place to let the team know is through [GitHub discussions](https://github.com/outline/outline/discussions).
|
||||
@@ -1,16 +1,17 @@
|
||||
Outline is a place to build your team knowledge base, you could think of it like your team’s shared library – a place for important documentation, notes, and ideas to live and be discovered. Some things you might want to keep in Outline:
|
||||
Outline is a place to build your team knowledge base, you could think of it like your team’s shared library – a place for important documentation, notes, and ideas to live and be discovered. Some things you might want to keep in Outline include:
|
||||
|
||||
\
|
||||
- Documentation
|
||||
- Support knowledge base
|
||||
- Product plans and RFCs
|
||||
- Sales playbooks
|
||||
- Support scripts
|
||||
- Onboarding
|
||||
- HR documents
|
||||
- Onboarding checklists
|
||||
- Company policies
|
||||
- Meeting notes
|
||||
- …and more
|
||||
|
||||
## Structure
|
||||
|
||||
Outline allows you to organize documents in "collections", for example these could represent topics like Sales, Product, or HR. Within collections documents can be interlinked and deeply nested to easily build relationships within your knowledge base.
|
||||
Outline allows you to organize documents in "collections", for example these could represent topics like Sales, Product, or HR. You can assign users or groups access to collections. Within collections documents can be interlinked and deeply nested to easily build relationships within your knowledge base.
|
||||
|
||||
## Search
|
||||
|
||||
|
||||
@@ -11,6 +11,13 @@ allow(User, "createDocument", Team, (user, team) => {
|
||||
});
|
||||
|
||||
allow(User, ["read", "download"], Document, (user, document) => {
|
||||
if (
|
||||
!document.fromShare &&
|
||||
!document.publishedAt &&
|
||||
document.createdById !== user.id
|
||||
)
|
||||
return false;
|
||||
|
||||
// existence of collection option is not required here to account for share tokens
|
||||
if (document.collection && cannot(user, "read", document.collection)) {
|
||||
return false;
|
||||
@@ -23,6 +30,7 @@ allow(User, ["star", "unstar"], Document, (user, document) => {
|
||||
if (document.archivedAt) return false;
|
||||
if (document.deletedAt) return false;
|
||||
if (document.template) return false;
|
||||
if (!document.publishedAt && document.createdById !== user.id) return false;
|
||||
|
||||
invariant(
|
||||
document.collection,
|
||||
@@ -36,6 +44,7 @@ allow(User, ["star", "unstar"], Document, (user, document) => {
|
||||
allow(User, "share", Document, (user, document) => {
|
||||
if (document.archivedAt) return false;
|
||||
if (document.deletedAt) return false;
|
||||
if (!document.publishedAt && document.createdById !== user.id) return false;
|
||||
|
||||
if (cannot(user, "share", document.collection)) {
|
||||
return false;
|
||||
@@ -47,6 +56,7 @@ allow(User, "share", Document, (user, document) => {
|
||||
allow(User, "update", Document, (user, document) => {
|
||||
if (document.archivedAt) return false;
|
||||
if (document.deletedAt) return false;
|
||||
if (!document.publishedAt && document.createdById !== user.id) return false;
|
||||
|
||||
if (cannot(user, "update", document.collection)) {
|
||||
return false;
|
||||
@@ -102,6 +112,7 @@ allow(User, ["pin", "unpin"], Document, (user, document) => {
|
||||
allow(User, "delete", Document, (user, document) => {
|
||||
if (user.isViewer) return false;
|
||||
if (document.deletedAt) return false;
|
||||
if (!document.publishedAt && document.createdById !== user.id) return false;
|
||||
|
||||
// allow deleting document without a collection
|
||||
if (document.collection && cannot(user, "update", document.collection)) {
|
||||
@@ -109,11 +120,7 @@ allow(User, "delete", Document, (user, document) => {
|
||||
}
|
||||
|
||||
// unpublished drafts can always be deleted
|
||||
if (
|
||||
!document.deletedAt &&
|
||||
!document.publishedAt &&
|
||||
user.teamId === document.teamId
|
||||
) {
|
||||
if (!document.publishedAt && user.teamId === document.teamId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,11 @@ export default class Notifications {
|
||||
event.name === "documents.publish" ? "published" : "updated";
|
||||
|
||||
for (const setting of notificationSettings) {
|
||||
// Suppress notifications for suspended users
|
||||
if (setting.user.isSuspended) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -70,14 +75,14 @@ export default class Notifications {
|
||||
eventName === "updated" &&
|
||||
!document.collaboratorIds.includes(setting.userId)
|
||||
) {
|
||||
return;
|
||||
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)) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If this user has viewed the document since the last update was made
|
||||
@@ -96,7 +101,7 @@ export default class Notifications {
|
||||
log(
|
||||
`suppressing notification to ${setting.userId} because update viewed`
|
||||
);
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
mailer.documentNotification({
|
||||
@@ -141,14 +146,19 @@ export default class Notifications {
|
||||
],
|
||||
});
|
||||
|
||||
notificationSettings.forEach((setting) =>
|
||||
for (const setting of notificationSettings) {
|
||||
// Suppress notifications for suspended users
|
||||
if (setting.user.isSuspended) {
|
||||
continue;
|
||||
}
|
||||
|
||||
mailer.collectionNotification({
|
||||
to: setting.user.email,
|
||||
eventName: "created",
|
||||
collection,
|
||||
actor: collection.user,
|
||||
unsubscribeUrl: setting.unsubscribeUrl,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as Sentry from "@sentry/node";
|
||||
import { addMonths } from "date-fns";
|
||||
import { type Context } from "koa";
|
||||
import { pick } from "lodash";
|
||||
import { User, Event, Team } from "../models";
|
||||
import { User, Event, Team, Collection, View } from "../models";
|
||||
import { getCookieDomain } from "../utils/domains";
|
||||
|
||||
export function getAllowedDomains(): string[] {
|
||||
@@ -99,6 +99,22 @@ export async function signIn(
|
||||
httpOnly: false,
|
||||
expires,
|
||||
});
|
||||
ctx.redirect(`${team.url}/home${isNewUser ? "?welcome" : ""}`);
|
||||
|
||||
const [collection, view] = await Promise.all([
|
||||
Collection.findOne({
|
||||
where: { teamId: user.teamId },
|
||||
order: [["index", "ASC"]],
|
||||
}),
|
||||
View.findOne({
|
||||
where: { userId: user.id },
|
||||
}),
|
||||
]);
|
||||
|
||||
const hasViewedDocuments = !!view;
|
||||
ctx.redirect(
|
||||
!hasViewedDocuments && collection
|
||||
? `${team.url}${collection.url}`
|
||||
: `${team.url}/home`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"Drafts": "Entwürfe",
|
||||
"Templates": "Vorlagen",
|
||||
"Deleted Collection": "Sammlung löschen",
|
||||
"History": "Verlauf",
|
||||
"Oh weird, there's nothing here": "Oh weird, there's nothing here",
|
||||
"New": "Neu",
|
||||
"Only visible to you": "Nur für Sie sichtbar",
|
||||
"Draft": "Entwurf",
|
||||
@@ -34,11 +36,11 @@
|
||||
"only you": "Nur Du",
|
||||
"person": "Person",
|
||||
"people": "Personen",
|
||||
"{{ total }} task": "{{ total }} task",
|
||||
"{{ total }} task_plural": "{{ total }} tasks",
|
||||
"{{ completed }} task done": "{{ completed }} task done",
|
||||
"{{ completed }} task done_plural": "{{ completed }} tasks done",
|
||||
"{{ completed }} of {{ total }} tasks": "{{ completed }} of {{ total }} tasks",
|
||||
"{{ total }} task": "{{ total }} Aufgabe",
|
||||
"{{ total }} task_plural": "{{ total }} Aufgaben",
|
||||
"{{ completed }} task done": "{{ completed }} Aufgabe erledigt",
|
||||
"{{ completed }} task done_plural": "{{ completed }} Aufgaben erledigt",
|
||||
"{{ completed }} of {{ total }} tasks": "{{ completed }} von {{ total }} Aufgaben",
|
||||
"Currently editing": "Derzeit in Bearbeitung",
|
||||
"Currently viewing": "Gerade angezeigt",
|
||||
"Viewed {{ timeAgo }} ago": "Angesehen vor {{ timeAgo }}",
|
||||
@@ -50,7 +52,7 @@
|
||||
"Align left": "Links ausrichten",
|
||||
"Align right": "Rechts ausrichten",
|
||||
"Bulleted list": "Punkteliste",
|
||||
"Todo list": "Task list",
|
||||
"Todo list": "Aufgabenliste",
|
||||
"Code block": "Codeblock",
|
||||
"Copied to clipboard": "In die Zwischenablage kopiert",
|
||||
"Code": "Code",
|
||||
@@ -75,7 +77,7 @@
|
||||
"Divider": "Trennlinie",
|
||||
"Image": "Bild",
|
||||
"Sorry, an error occurred uploading the image": "Beim Hochladen des Bildes ist ein Fehler aufgetreten",
|
||||
"Write a caption": "Write a caption",
|
||||
"Write a caption": "Untertitel schreiben",
|
||||
"Info": "Info",
|
||||
"Info notice": "Info-Hinweis",
|
||||
"Link": "Link",
|
||||
@@ -101,20 +103,28 @@
|
||||
"Tip notice": "Tipp Hinweis",
|
||||
"Warning": "Warnung",
|
||||
"Warning notice": "Warnhinweis",
|
||||
"Module failed to load": "Module failed to load",
|
||||
"Loading Failed": "Loading Failed",
|
||||
"Sorry, part of the application failed to load. This may be because it was updated since you opened the tab or because of a failed network request. Please try reloading.": "Sorry, part of the application failed to load. This may be because it was updated since you opened the tab or because of a failed network request. Please try reloading.",
|
||||
"Reload": "Reload",
|
||||
"Something Unexpected Happened": "Something Unexpected Happened",
|
||||
"Sorry, an unrecoverable error occurred{{notified}}. Please try reloading the page, it may have been a temporary glitch.": "Sorry, an unrecoverable error occurred{{notified}}. Please try reloading the page, it may have been a temporary glitch.",
|
||||
"our engineers have been notified": "our engineers have been notified",
|
||||
"Report a Bug": "Report a Bug",
|
||||
"Show Detail": "Show Detail",
|
||||
"Module failed to load": "Modul konnte nicht geladen werden",
|
||||
"Loading Failed": "Laden fehlgeschlagen",
|
||||
"Sorry, part of the application failed to load. This may be because it was updated since you opened the tab or because of a failed network request. Please try reloading.": "Leider konnte ein Teil der Software nicht geladen werden weil der Inhalt sich inzwischen geändert hat oder eine Abfrage nicht erfolgreich abgeschlossen werden konnte. Bitte neu laden.",
|
||||
"Reload": "Neu Laden",
|
||||
"Something Unexpected Happened": "Ein unerwarteter Fehler ist aufgetreten",
|
||||
"Sorry, an unrecoverable error occurred{{notified}}. Please try reloading the page, it may have been a temporary glitch.": "Entschuldigung, ein nicht behebbarer Fehler ist aufgetreten{{notified}}. Bitte versuchen Sie die Seite neu zu laden, es könnte inzwischen wieder funktionieren.",
|
||||
"our engineers have been notified": "unsere Entwickler wurden benachrichtigt",
|
||||
"Report a Bug": "Fehler melden",
|
||||
"Show Detail": "Details anzeigen",
|
||||
"Latest version": "Latest version",
|
||||
"{{userName}} edited": "{{userName}} edited",
|
||||
"{{userName}} archived": "{{userName}} archived",
|
||||
"{{userName}} restored": "{{userName}} restored",
|
||||
"{{userName}} deleted": "{{userName}} deleted",
|
||||
"{{userName}} moved from trash": "{{userName}} moved from trash",
|
||||
"{{userName}} published": "{{userName}} published",
|
||||
"{{userName}} moved": "{{userName}} moved",
|
||||
"Icon": "Icon",
|
||||
"Show menu": "Menü anzeigen",
|
||||
"Choose icon": "Icon auswählen",
|
||||
"Loading": "Laden",
|
||||
"Loading editor": "Loading editor",
|
||||
"Loading editor": "Editor wird geladen",
|
||||
"Search": "Suche",
|
||||
"Default access": "Standardzugriff",
|
||||
"View and edit": "Anzeigen und bearbeiten",
|
||||
@@ -198,13 +208,12 @@
|
||||
"Create template": "Vorlage erstellen",
|
||||
"Duplicate": "Duplizieren",
|
||||
"Unpublish": "Nicht veröffentlichen",
|
||||
"Permanently delete": "Permanently delete",
|
||||
"Permanently delete": "Unwiderruflich löschen",
|
||||
"Move": "Verschieben",
|
||||
"History": "Verlauf",
|
||||
"Download": "Herunterladen",
|
||||
"Print": "Drucken",
|
||||
"Move {{ documentName }}": "Verschiebe {{ documentName }}",
|
||||
"Permanently delete {{ documentName }}": "Permanently delete {{ documentName }}",
|
||||
"Permanently delete {{ documentName }}": "{{ documentName }} endgültig löschen",
|
||||
"Edit group": "Gruppe bearbeiten",
|
||||
"Delete group": "Gruppe löschen",
|
||||
"Group options": "Gruppen-Einstellungen",
|
||||
@@ -222,8 +231,8 @@
|
||||
"Share options": "Teilen-Einstellungen",
|
||||
"Go to document": "Zum Dokument gehen",
|
||||
"Revoke link": "Link widerrufen",
|
||||
"Contents": "Contents",
|
||||
"Headings you add to the document will appear here": "Headings you add to the document will appear here",
|
||||
"Contents": "Inhalte",
|
||||
"Headings you add to the document will appear here": "Überschriften, die Sie dem Dokument hinzufügen, werden hier angezeigt",
|
||||
"Table of contents": "Inhaltsverzeichnis",
|
||||
"By {{ author }}": "Von {{ author }}",
|
||||
"Are you sure you want to make {{ userName }} an admin? Admins can modify team and billing information.": "Sind Sie sicher, dass Sie {{ userName }} zu einem Administrator machen möchten? Administratoren können Team- und Rechnungsinformationen ändern.",
|
||||
@@ -237,8 +246,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\".",
|
||||
"API token created": "API-Token erstellt",
|
||||
"Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".": "Nennen Sie Ihren Token so, dass Sie sich leicht daran erinnern können, z. B. \"development\" \"production\" oder \"continuous integration\".",
|
||||
"Documents": "Dokumente",
|
||||
"The document archive is empty at the moment.": "Das Dokumentenarchiv ist momentan leer.",
|
||||
"Search in collection": "Suche in Sammlung",
|
||||
@@ -254,7 +263,7 @@
|
||||
"Least recently updated": "Am längsten nicht aktualisiert",
|
||||
"A–Z": "A - Z",
|
||||
"Drop documents to import": "Dokumente zum Importieren hier ablegen",
|
||||
"Are you sure about that? Deleting the <em>{{collectionName}}</em> collection is permanent and cannot be restored, however documents within will be moved to the trash.": "Are you sure about that? Deleting the <em>{{collectionName}}</em> collection is permanent and cannot be restored, however documents within will be moved to the trash.",
|
||||
"Are you sure about that? Deleting the <em>{{collectionName}}</em> collection is permanent and cannot be restored, however documents within will be moved to the trash.": "Sind Sie sich sicher? Löschung der <em>{{collectionName}}</em> Sammlung ist dauerhaft und nicht wiederrufbar. Die Dokumente selbst werden jedoch dem Papierkorb hinzugefügt.",
|
||||
"Deleting": "Wird gelöscht",
|
||||
"I’m sure – Delete": "Ich bin mir sicher – Löschen",
|
||||
"The collection was updated": "Die Sammlung wurde aktualisiert",
|
||||
@@ -266,9 +275,9 @@
|
||||
"Public sharing is currently disabled in the team security settings.": "Öffentliches Teilen ist derzeit in den Sicherheitseinstellungen des Teams deaktiviert.",
|
||||
"Saving": "Speichert",
|
||||
"Save": "Speichern",
|
||||
"Exporting the collection <em>{{collectionName}}</em> may take a few seconds. Your documents will be downloaded as a zip of folders with files in Markdown format.": "Exporting the collection <em>{{collectionName}}</em> may take a few seconds. Your documents will be downloaded as a zip of folders with files in Markdown format.",
|
||||
"Exporting": "Exporting",
|
||||
"Export Collection": "Export Collection",
|
||||
"Exporting the collection <em>{{collectionName}}</em> may take a few seconds. Your documents will be downloaded as a zip of folders with files in Markdown format.": "Der Export der Sammlung <em>{{collectionName}}</em> kann einige Sekunden dauern. Ihre Dokumente werden als ZIP-Datei mit Markdown Textdateien heruntergeladen.",
|
||||
"Exporting": "Wird exportiert",
|
||||
"Export Collection": "Sammlung exportieren",
|
||||
"Collections are for grouping your documents. They work best when organized around a topic or internal team — Product or Engineering for example.": "Sammlungen dienen zur Gruppierung von Dokumenten. Sie funktionieren am besten, wenn sie nach einem Thema oder nach internen Teams organisiert sind — z. B. Produkt oder Entwicklung.",
|
||||
"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.": "Dies ist die Standardstufe des Zugriffs für Teammitglieder. Du kannst bestimmten Nutzern oder Gruppen mehr Zugriff geben, sobald die Sammlung erstellt wurde.",
|
||||
"Creating": "Wird erstellt",
|
||||
@@ -311,17 +320,17 @@
|
||||
"Add specific access for individual groups and team members": "Speziellen Zugriff für einzelne Gruppen und Teammitglieder hinzufügen",
|
||||
"Add groups to {{ collectionName }}": "Gruppen zu {{ collectionName }} hinzufügen",
|
||||
"Add people to {{ collectionName }}": "Personen zu {{ collectionName }} hinzufügen",
|
||||
"Document updated by {{userName}}": "Document updated by {{userName}}",
|
||||
"You have unsaved changes.\nAre you sure you want to discard them?": "You have unsaved changes.\nAre you sure you want to discard them?",
|
||||
"Images are still uploading.\nAre you sure you want to discard them?": "Images are still uploading.\nAre you sure you want to discard them?",
|
||||
"You’re editing a template. Highlight some text and use the <2></2> control to add placeholders that can be filled out when creating new documents from this template.": "You’re editing a template. Highlight some text and use the <2></2> control to add placeholders that can be filled out when creating new documents from this template.",
|
||||
"Archived by {{userName}}": "Archived by {{userName}}",
|
||||
"Deleted by {{userName}}": "Deleted by {{userName}}",
|
||||
"This template will be permanently deleted in <2></2> unless restored.": "This template will be permanently deleted in <2></2> unless restored.",
|
||||
"This document will be permanently deleted in <2></2> unless restored.": "This document will be permanently deleted in <2></2> unless restored.",
|
||||
"Start your template…": "Start your template…",
|
||||
"Start with a title…": "Start with a title…",
|
||||
"…the rest is up to you": "…the rest is up to you",
|
||||
"Document updated by {{userName}}": "Dokument aktualisiert durch {{userName}}",
|
||||
"You have unsaved changes.\nAre you sure you want to discard them?": "Sie haben ungespeicherte Änderungen. Sind Sie sicher, dass Sie abbrechen wollen?",
|
||||
"Images are still uploading.\nAre you sure you want to discard them?": "Bilder werden noch hochgeladen.\nMöchten Sie sie wirklich verwerfen?",
|
||||
"You’re editing a template. Highlight some text and use the <2></2> control to add placeholders that can be filled out when creating new documents from this template.": "Sie bearbeiten eine Vorlage. Markieren Sie eine Textstelle und verwenden Sie die <2></2> -control, um Platzhalter hinzuzufügen, die beim Erstellen neuer Dokumente aus dieser Vorlage ausgefüllt werden können.",
|
||||
"Archived by {{userName}}": "Archiviert durch {{userName}}",
|
||||
"Deleted by {{userName}}": "Gelöscht von {{userName}}",
|
||||
"This template will be permanently deleted in <2></2> unless restored.": "Diese Vorlage wird dauerhaft in <2></2> gelöscht, wenn sie nicht wiederhergestellt wird.",
|
||||
"This document will be permanently deleted in <2></2> unless restored.": "Dieses Dokument wird dauerhaft in <2></2> gelöscht, wenn es nicht wiederhergestellt wird.",
|
||||
"Start your template…": "Vorlage starten…",
|
||||
"Start with a title…": "Mit dem Titel beginnen…",
|
||||
"…the rest is up to you": "…der Rest liegt bei Ihnen",
|
||||
"Hide contents": "Inhalt ausblenden",
|
||||
"Show contents": "Inhalt anzeigen",
|
||||
"Edit {{noun}}": "{{noun}} bearbeiten",
|
||||
@@ -347,15 +356,15 @@
|
||||
"Are you sure about that? Deleting the <em>{{ documentTitle }}</em> document will delete all of its history and any nested documents.": "Bist du dir sicher? Durch Löschen des Dokuments <em>{{ documentTitle }}</em>, werden der gesamte Verlauf und alle Unterdokumente gelöscht.",
|
||||
"If you’d like the option of referencing or restoring the {{noun}} in the future, consider archiving it instead.": "Wenn du {{noun}} in Zukunft noch referenzieren oder wiederherstellen möchtest, solltest du es stattdessen archivieren.",
|
||||
"Archiving": "Wird archiviert",
|
||||
"Document moved": "Document moved",
|
||||
"Current location": "Current location",
|
||||
"Choose a new location": "Choose a new location",
|
||||
"Search collections & documents": "Search collections & documents",
|
||||
"Document moved": "Dokument verschoben",
|
||||
"Current location": "Aktueller Speicherort",
|
||||
"Choose a new location": "Wähle neuen Speicherort",
|
||||
"Search collections & documents": "Sammlungen und Dokumente durchsuchen",
|
||||
"Couldn’t 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.",
|
||||
"Template created, go ahead and customize it": "Template created, go ahead and customize it",
|
||||
"Creating a template from <em>{{titleWithDefault}}</em> is a non-destructive action – we'll make a copy of the document and turn it into a template that can be used as a starting point for new documents.": "Creating a template from <em>{{titleWithDefault}}</em> is a non-destructive action – we'll make a copy of the document and turn it into a template that can be used as a starting point for new documents.",
|
||||
"Document permanently deleted": "Dokument endgültig gelöscht",
|
||||
"Are you sure you want to permanently delete the <em>{{ documentTitle }}</em> document? This action is immediate and cannot be undone.": "Möchten Sie das Dokument <em>{{ documentTitle }}</em> dauerhaft löschen? Die Löschung erfolgt sofort und kann nicht rückgängig gemacht werden.",
|
||||
"Template created, go ahead and customize it": "Vorlage erstellt, fortfahren und anpassen",
|
||||
"Creating a template from <em>{{titleWithDefault}}</em> is a non-destructive action – we'll make a copy of the document and turn it into a template that can be used as a starting point for new documents.": "Das Erstellen einer Vorlage durch <em>{{titleWithDefault}}</em> ist eine non-destruktive Aktion – wir erstellen eine Kopie des Dokuments und verwandeln es in eine Vorlage, die als Ausgangspunkt für neue Dokumente verwendet werden kann.",
|
||||
"Search documents": "Dokumente durchsuchen",
|
||||
"No documents found for your filters.": "Keine Dokumente anhand Ihre Filter gefunden.",
|
||||
"You’ve not got any drafts at the moment.": "Sie haben im Moment keine Entwürfe.",
|
||||
@@ -365,8 +374,8 @@
|
||||
"We were unable to load the document while offline.": "Wir konnten das Dokument nicht offline laden.",
|
||||
"Your account has been suspended": "Ihr Konto wurde gesperrt",
|
||||
"A team admin (<em>{{ suspendedContactEmail }}</em>) has suspended your account. To re-activate your account, please reach out to them directly.": "Ein Administrator (<em>{{ suspendedContactEmail }}</em>) hat dein Konto gesperrt. Um dein Konto zu reaktivieren, wende dich bitte direkt an diesen.",
|
||||
"Are you sure about that? Deleting the <em>{{groupName}}</em> group will cause its members to lose access to collections and documents that it is associated with.": "Are you sure about that? Deleting the <em>{{groupName}}</em> group will cause its members to lose access to collections and documents that it is associated with.",
|
||||
"You can edit the name of this group at any time, however doing so too often might confuse your team mates.": "You can edit the name of this group at any time, however doing so too often might confuse your team mates.",
|
||||
"Are you sure about that? Deleting the <em>{{groupName}}</em> group will cause its members to lose access to collections and documents that it is associated with.": "Sind Sie sicher? Durch das Löschen der <em>{{groupName}}</em> verlieren Ihre Teammitglieder den Zugriff auf Sammlungen und Dokumente die mit der Gruppe verknüpft waren.",
|
||||
"You can edit the name of this group at any time, however doing so too often might confuse your team mates.": "Sie können den Namen dieser Gruppe jederzeit ändern. Dies kann jedoch Ihre Teammitglieder verwirren.",
|
||||
"{{userName}} was added to the group": "{{userName}} wurde zur Gruppe hinzugefügt",
|
||||
"Add team members below to give them access to the group. Need to add someone who’s not yet on the team yet?": "Fügen Sie unten Teammitglieder hinzu, um ihnen Zugriff auf die Gruppe zu gewähren. Sie müssen jemanden hinzufügen, der noch nicht im Team ist?",
|
||||
"Invite them to {{teamName}}": "Personen zu {{teamName}} einladen",
|
||||
@@ -552,5 +561,11 @@
|
||||
"Joined": "Beigetreten",
|
||||
"{{ time }} ago.": "Vor {{ time }}.",
|
||||
"Edit Profile": "Profil bearbeiten",
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} hat noch keine Dokumente aktualisiert."
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} hat noch keine Dokumente aktualisiert.",
|
||||
"Today": "Today",
|
||||
"Yesterday": "Yesterday",
|
||||
"Last week": "Last week",
|
||||
"This month": "This month",
|
||||
"Last month": "Last month",
|
||||
"This year": "This year"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"Drafts": "Drafts",
|
||||
"Templates": "Templates",
|
||||
"Deleted Collection": "Deleted Collection",
|
||||
"History": "History",
|
||||
"Oh weird, there's nothing here": "Oh weird, there's nothing here",
|
||||
"New": "New",
|
||||
"Only visible to you": "Only visible to you",
|
||||
"Draft": "Draft",
|
||||
@@ -110,6 +112,14 @@
|
||||
"our engineers have been notified": "our engineers have been notified",
|
||||
"Report a Bug": "Report a Bug",
|
||||
"Show Detail": "Show Detail",
|
||||
"Latest version": "Latest version",
|
||||
"{{userName}} edited": "{{userName}} edited",
|
||||
"{{userName}} archived": "{{userName}} archived",
|
||||
"{{userName}} restored": "{{userName}} restored",
|
||||
"{{userName}} deleted": "{{userName}} deleted",
|
||||
"{{userName}} moved from trash": "{{userName}} moved from trash",
|
||||
"{{userName}} published": "{{userName}} published",
|
||||
"{{userName}} moved": "{{userName}} moved",
|
||||
"Icon": "Icon",
|
||||
"Show menu": "Show menu",
|
||||
"Choose icon": "Choose icon",
|
||||
@@ -131,9 +141,12 @@
|
||||
"Collections": "Collections",
|
||||
"Untitled": "Untitled",
|
||||
"Document not supported – try Markdown, Plain text, HTML, or Word": "Document not supported – try Markdown, Plain text, HTML, or Word",
|
||||
"Starred documents could not be loaded": "Starred documents could not be loaded",
|
||||
"Starred": "Starred",
|
||||
"Show more": "Show more",
|
||||
"Show less": "Show less",
|
||||
"Delete {{ documentName }}": "Delete {{ documentName }}",
|
||||
"Home": "Home",
|
||||
"Starred": "Starred",
|
||||
"Settings": "Settings",
|
||||
"Invite people": "Invite people",
|
||||
"Create a collection": "Create a collection",
|
||||
@@ -157,14 +170,15 @@
|
||||
"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",
|
||||
"Appearance": "Appearance",
|
||||
"System": "System",
|
||||
"Light": "Light",
|
||||
"Dark": "Dark",
|
||||
"Switch team": "Switch team",
|
||||
"Log out": "Log out",
|
||||
"Show path to document": "Show path to document",
|
||||
"Path to document": "Path to document",
|
||||
@@ -192,15 +206,14 @@
|
||||
"Choose a collection": "Choose a collection",
|
||||
"Unpin": "Unpin",
|
||||
"Pin to collection": "Pin to collection",
|
||||
"Enable embeds": "Enable embeds",
|
||||
"Disable embeds": "Disable embeds",
|
||||
"New nested document": "New nested document",
|
||||
"Create template": "Create template",
|
||||
"Duplicate": "Duplicate",
|
||||
"Unpublish": "Unpublish",
|
||||
"Permanently delete": "Permanently delete",
|
||||
"Move": "Move",
|
||||
"History": "History",
|
||||
"Enable embeds": "Enable embeds",
|
||||
"Disable embeds": "Disable embeds",
|
||||
"Download": "Download",
|
||||
"Print": "Print",
|
||||
"Move {{ documentName }}": "Move {{ documentName }}",
|
||||
@@ -382,6 +395,7 @@
|
||||
"Recently viewed": "Recently viewed",
|
||||
"Created by me": "Created by me",
|
||||
"Weird, this shouldn’t ever be empty": "Weird, this shouldn’t ever be empty",
|
||||
"You haven’t created any documents yet": "You haven’t created any documents yet",
|
||||
"Documents you’ve recently viewed will be here for easy access": "Documents you’ve recently viewed will be here for easy access",
|
||||
"We sent out your invites!": "We sent out your invites!",
|
||||
"Sorry, you can only send {{MAX_INVITES}} invites at a time": "Sorry, you can only send {{MAX_INVITES}} invites at a time",
|
||||
@@ -541,7 +555,6 @@
|
||||
"Zapier": "Zapier",
|
||||
"Zapier is a platform that allows Outline to easily integrate with thousands of other business tools. Head over to Zapier to setup a \"Zap\" and start programmatically interacting with Outline.'": "Zapier is a platform that allows Outline to easily integrate with thousands of other business tools. Head over to Zapier to setup a \"Zap\" and start programmatically interacting with Outline.'",
|
||||
"Open Zapier": "Open Zapier",
|
||||
"You’ve not starred any documents yet.": "You’ve 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.",
|
||||
@@ -552,5 +565,11 @@
|
||||
"Joined": "Joined",
|
||||
"{{ time }} ago.": "{{ time }} ago.",
|
||||
"Edit Profile": "Edit Profile",
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} hasn’t updated any documents yet."
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} hasn’t updated any documents yet.",
|
||||
"Today": "Today",
|
||||
"Yesterday": "Yesterday",
|
||||
"Last week": "Last week",
|
||||
"This month": "This month",
|
||||
"Last month": "Last month",
|
||||
"This year": "This year"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"Drafts": "Borradores",
|
||||
"Templates": "Plantillas",
|
||||
"Deleted Collection": "Colección Eliminada",
|
||||
"History": "Historial",
|
||||
"Oh weird, there's nothing here": "Oh weird, there's nothing here",
|
||||
"New": "Nuevo",
|
||||
"Only visible to you": "Solo visible para ti",
|
||||
"Draft": "Borrador",
|
||||
@@ -110,6 +112,14 @@
|
||||
"our engineers have been notified": "our engineers have been notified",
|
||||
"Report a Bug": "Report a Bug",
|
||||
"Show Detail": "Show Detail",
|
||||
"Latest version": "Latest version",
|
||||
"{{userName}} edited": "{{userName}} edited",
|
||||
"{{userName}} archived": "{{userName}} archived",
|
||||
"{{userName}} restored": "{{userName}} restored",
|
||||
"{{userName}} deleted": "{{userName}} deleted",
|
||||
"{{userName}} moved from trash": "{{userName}} moved from trash",
|
||||
"{{userName}} published": "{{userName}} published",
|
||||
"{{userName}} moved": "{{userName}} moved",
|
||||
"Icon": "Ícono",
|
||||
"Show menu": "Mostrar menú",
|
||||
"Choose icon": "Seleccionar icono",
|
||||
@@ -200,7 +210,6 @@
|
||||
"Unpublish": "Cancelar publicación",
|
||||
"Permanently delete": "Eliminar permanentemente",
|
||||
"Move": "Mover",
|
||||
"History": "Historial",
|
||||
"Download": "Descargar",
|
||||
"Print": "Imprimir",
|
||||
"Move {{ documentName }}": "Mover {{ documentName }}",
|
||||
@@ -552,5 +561,11 @@
|
||||
"Joined": "Unido",
|
||||
"{{ time }} ago.": "Hace {{ time }}.",
|
||||
"Edit Profile": "Editar perfil",
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} aún no ha actualizado ningún documento."
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} aún no ha actualizado ningún documento.",
|
||||
"Today": "Today",
|
||||
"Yesterday": "Yesterday",
|
||||
"Last week": "Last week",
|
||||
"This month": "This month",
|
||||
"Last month": "Last month",
|
||||
"This year": "This year"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"Drafts": "پیشنویسها",
|
||||
"Templates": "قالبها",
|
||||
"Deleted Collection": "مجموعههای حذف شده",
|
||||
"History": "تاریخچه",
|
||||
"Oh weird, there's nothing here": "Oh weird, there's nothing here",
|
||||
"New": "جدید",
|
||||
"Only visible to you": "تنها قابل مشاهده برای شما",
|
||||
"Draft": "پیشنویس",
|
||||
@@ -110,6 +112,14 @@
|
||||
"our engineers have been notified": "به مهندسان ما اطلاعرسانی شد",
|
||||
"Report a Bug": "گزارش اشکال",
|
||||
"Show Detail": "نمایش جزئیات",
|
||||
"Latest version": "Latest version",
|
||||
"{{userName}} edited": "{{userName}} edited",
|
||||
"{{userName}} archived": "{{userName}} archived",
|
||||
"{{userName}} restored": "{{userName}} restored",
|
||||
"{{userName}} deleted": "{{userName}} deleted",
|
||||
"{{userName}} moved from trash": "{{userName}} moved from trash",
|
||||
"{{userName}} published": "{{userName}} published",
|
||||
"{{userName}} moved": "{{userName}} moved",
|
||||
"Icon": "نماد",
|
||||
"Show menu": "نمایش منو",
|
||||
"Choose icon": "انتخاب نماد",
|
||||
@@ -200,7 +210,6 @@
|
||||
"Unpublish": "لغو انتشار",
|
||||
"Permanently delete": "حذف برای همیشه",
|
||||
"Move": "انتقال",
|
||||
"History": "تاریخچه",
|
||||
"Download": "بارگیری",
|
||||
"Print": "چاپ",
|
||||
"Move {{ documentName }}": "انتقال {{ documentName }}",
|
||||
@@ -552,5 +561,11 @@
|
||||
"Joined": "پیوست",
|
||||
"{{ time }} ago.": "{{ time }} پیش.",
|
||||
"Edit Profile": "ویرایش پروفایل",
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} هنوز هیچ سندی را بهروز نکرده است."
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} هنوز هیچ سندی را بهروز نکرده است.",
|
||||
"Today": "Today",
|
||||
"Yesterday": "Yesterday",
|
||||
"Last week": "Last week",
|
||||
"This month": "This month",
|
||||
"Last month": "Last month",
|
||||
"This year": "This year"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"Drafts": "Brouillons",
|
||||
"Templates": "Modèles",
|
||||
"Deleted Collection": "Collection supprimée",
|
||||
"History": "Historique",
|
||||
"Oh weird, there's nothing here": "Oh weird, there's nothing here",
|
||||
"New": "Nouveau",
|
||||
"Only visible to you": "Visible uniquement pour vous",
|
||||
"Draft": "Brouillon",
|
||||
@@ -110,6 +112,14 @@
|
||||
"our engineers have been notified": "our engineers have been notified",
|
||||
"Report a Bug": "Report a Bug",
|
||||
"Show Detail": "Show Detail",
|
||||
"Latest version": "Latest version",
|
||||
"{{userName}} edited": "{{userName}} edited",
|
||||
"{{userName}} archived": "{{userName}} archived",
|
||||
"{{userName}} restored": "{{userName}} restored",
|
||||
"{{userName}} deleted": "{{userName}} deleted",
|
||||
"{{userName}} moved from trash": "{{userName}} moved from trash",
|
||||
"{{userName}} published": "{{userName}} published",
|
||||
"{{userName}} moved": "{{userName}} moved",
|
||||
"Icon": "Icône",
|
||||
"Show menu": "Afficher le menu",
|
||||
"Choose icon": "Choisir une icône",
|
||||
@@ -200,7 +210,6 @@
|
||||
"Unpublish": "Dépublier",
|
||||
"Permanently delete": "Supprimer définitivement",
|
||||
"Move": "Déplacer",
|
||||
"History": "Historique",
|
||||
"Download": "Télécharger",
|
||||
"Print": "Imprimer",
|
||||
"Move {{ documentName }}": "Déplacer {{ documentName }}",
|
||||
@@ -552,5 +561,11 @@
|
||||
"Joined": "Rejoint",
|
||||
"{{ time }} ago.": "Il y a {{ time }}.",
|
||||
"Edit Profile": "Modifier le profil",
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} n'a pas encore mis à jour de document."
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} n'a pas encore mis à jour de document.",
|
||||
"Today": "Today",
|
||||
"Yesterday": "Yesterday",
|
||||
"Last week": "Last week",
|
||||
"This month": "This month",
|
||||
"Last month": "Last month",
|
||||
"This year": "This year"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"Drafts": "Bozze",
|
||||
"Templates": "Templates",
|
||||
"Deleted Collection": "Raccolte Eliminate",
|
||||
"History": "Cronologia",
|
||||
"Oh weird, there's nothing here": "Oh weird, there's nothing here",
|
||||
"New": "Nuovo",
|
||||
"Only visible to you": "Visibile solo a te",
|
||||
"Draft": "Bozza",
|
||||
@@ -110,6 +112,14 @@
|
||||
"our engineers have been notified": "our engineers have been notified",
|
||||
"Report a Bug": "Report a Bug",
|
||||
"Show Detail": "Show Detail",
|
||||
"Latest version": "Latest version",
|
||||
"{{userName}} edited": "{{userName}} edited",
|
||||
"{{userName}} archived": "{{userName}} archived",
|
||||
"{{userName}} restored": "{{userName}} restored",
|
||||
"{{userName}} deleted": "{{userName}} deleted",
|
||||
"{{userName}} moved from trash": "{{userName}} moved from trash",
|
||||
"{{userName}} published": "{{userName}} published",
|
||||
"{{userName}} moved": "{{userName}} moved",
|
||||
"Icon": "Icona",
|
||||
"Show menu": "Mostra menu",
|
||||
"Choose icon": "Scegli icona",
|
||||
@@ -200,7 +210,6 @@
|
||||
"Unpublish": "Depubblica",
|
||||
"Permanently delete": "Permanently delete",
|
||||
"Move": "Sposta",
|
||||
"History": "Cronologia",
|
||||
"Download": "Download",
|
||||
"Print": "Stampa",
|
||||
"Move {{ documentName }}": "Move {{ documentName }}",
|
||||
@@ -552,5 +561,11 @@
|
||||
"Joined": "Iscritto",
|
||||
"{{ time }} ago.": "{{ time }} fa.",
|
||||
"Edit Profile": "Modifica profilo",
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} non ha ancora aggiornato nessun documento."
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} non ha ancora aggiornato nessun documento.",
|
||||
"Today": "Today",
|
||||
"Yesterday": "Yesterday",
|
||||
"Last week": "Last week",
|
||||
"This month": "This month",
|
||||
"Last month": "Last month",
|
||||
"This year": "This year"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"Drafts": "下書き",
|
||||
"Templates": "テンプレート",
|
||||
"Deleted Collection": "削除したコレクション",
|
||||
"History": "変更履歴",
|
||||
"Oh weird, there's nothing here": "Oh weird, there's nothing here",
|
||||
"New": "新規作成",
|
||||
"Only visible to you": "あなたにしか見えない",
|
||||
"Draft": "下書き",
|
||||
@@ -110,6 +112,14 @@
|
||||
"our engineers have been notified": "our engineers have been notified",
|
||||
"Report a Bug": "Report a Bug",
|
||||
"Show Detail": "Show Detail",
|
||||
"Latest version": "Latest version",
|
||||
"{{userName}} edited": "{{userName}} edited",
|
||||
"{{userName}} archived": "{{userName}} archived",
|
||||
"{{userName}} restored": "{{userName}} restored",
|
||||
"{{userName}} deleted": "{{userName}} deleted",
|
||||
"{{userName}} moved from trash": "{{userName}} moved from trash",
|
||||
"{{userName}} published": "{{userName}} published",
|
||||
"{{userName}} moved": "{{userName}} moved",
|
||||
"Icon": "アイコン",
|
||||
"Show menu": "メニューを表示",
|
||||
"Choose icon": "アイコンの選択",
|
||||
@@ -200,7 +210,6 @@
|
||||
"Unpublish": "未発表",
|
||||
"Permanently delete": "完全に削除します。",
|
||||
"Move": "移動",
|
||||
"History": "変更履歴",
|
||||
"Download": "ダウンロード",
|
||||
"Print": "プリント",
|
||||
"Move {{ documentName }}": "{{ documentName }} を移動",
|
||||
@@ -552,5 +561,11 @@
|
||||
"Joined": "参加しました",
|
||||
"{{ time }} ago.": "{{ time }} 前",
|
||||
"Edit Profile": "プロフィールの編集",
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }}はまだ文書を更新していません"
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }}はまだ文書を更新していません",
|
||||
"Today": "Today",
|
||||
"Yesterday": "Yesterday",
|
||||
"Last week": "Last week",
|
||||
"This month": "This month",
|
||||
"Last month": "Last month",
|
||||
"This year": "This year"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"Drafts": "임시 보관함",
|
||||
"Templates": "템플릿",
|
||||
"Deleted Collection": "삭제 된 콜렉션",
|
||||
"History": "히스토리",
|
||||
"Oh weird, there's nothing here": "Oh weird, there's nothing here",
|
||||
"New": "신규",
|
||||
"Only visible to you": "나에게만 보임",
|
||||
"Draft": "임시보관",
|
||||
@@ -110,6 +112,14 @@
|
||||
"our engineers have been notified": "our engineers have been notified",
|
||||
"Report a Bug": "Report a Bug",
|
||||
"Show Detail": "Show Detail",
|
||||
"Latest version": "Latest version",
|
||||
"{{userName}} edited": "{{userName}} edited",
|
||||
"{{userName}} archived": "{{userName}} archived",
|
||||
"{{userName}} restored": "{{userName}} restored",
|
||||
"{{userName}} deleted": "{{userName}} deleted",
|
||||
"{{userName}} moved from trash": "{{userName}} moved from trash",
|
||||
"{{userName}} published": "{{userName}} published",
|
||||
"{{userName}} moved": "{{userName}} moved",
|
||||
"Icon": "아이콘",
|
||||
"Show menu": "메뉴 보기",
|
||||
"Choose icon": "아이콘 선택",
|
||||
@@ -200,7 +210,6 @@
|
||||
"Unpublish": "게시 취소",
|
||||
"Permanently delete": "Permanently delete",
|
||||
"Move": "이동",
|
||||
"History": "히스토리",
|
||||
"Download": "다운로드",
|
||||
"Print": "인쇄",
|
||||
"Move {{ documentName }}": "{{ documentName }} 를 이동함",
|
||||
@@ -552,5 +561,11 @@
|
||||
"Joined": "참여함",
|
||||
"{{ time }} ago.": "{{ time }} 전.",
|
||||
"Edit Profile": "프로필 수정",
|
||||
"{{ userName }} hasn’t updated any documents yet.": "아직 {{ userName }} 가 문서를 업데이트하지 않았습니다."
|
||||
"{{ userName }} hasn’t updated any documents yet.": "아직 {{ userName }} 가 문서를 업데이트하지 않았습니다.",
|
||||
"Today": "Today",
|
||||
"Yesterday": "Yesterday",
|
||||
"Last week": "Last week",
|
||||
"This month": "This month",
|
||||
"Last month": "Last month",
|
||||
"This year": "This year"
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
{
|
||||
"currently editing": "momenteel aan het bewerken",
|
||||
"currently viewing": "momenteen aan het bekijken",
|
||||
"previously edited": "previously edited",
|
||||
"previously edited": "eerder bewerkt",
|
||||
"You": "Jij",
|
||||
"Viewers": "Kijkers",
|
||||
"Sorry, an error occurred saving the collection": "Er is een fout opgetreden bij het opslaan van de collectie",
|
||||
"Add a description": "Voeg een beschrijving toe",
|
||||
"Collapse": "Collapse",
|
||||
"Expand": "Expand",
|
||||
"Collapse": "Inklappen",
|
||||
"Expand": "Uitklappen",
|
||||
"Submenu": "Submenu",
|
||||
"Trash": "Prullenmand",
|
||||
"Archive": "Archief",
|
||||
"Drafts": "Concepten",
|
||||
"Templates": "Sjablonen",
|
||||
"Deleted Collection": "Collectie verwijderd",
|
||||
"History": "Geschiedenis",
|
||||
"Oh weird, there's nothing here": "Oh weird, there's nothing here",
|
||||
"New": "Nieuw",
|
||||
"Only visible to you": "Alleen zichtbaar voor jou",
|
||||
"Draft": "Concept",
|
||||
@@ -110,6 +112,14 @@
|
||||
"our engineers have been notified": "our engineers have been notified",
|
||||
"Report a Bug": "Report a Bug",
|
||||
"Show Detail": "Show Detail",
|
||||
"Latest version": "Latest version",
|
||||
"{{userName}} edited": "{{userName}} edited",
|
||||
"{{userName}} archived": "{{userName}} archived",
|
||||
"{{userName}} restored": "{{userName}} restored",
|
||||
"{{userName}} deleted": "{{userName}} deleted",
|
||||
"{{userName}} moved from trash": "{{userName}} moved from trash",
|
||||
"{{userName}} published": "{{userName}} published",
|
||||
"{{userName}} moved": "{{userName}} moved",
|
||||
"Icon": "Icoon",
|
||||
"Show menu": "Toon menu",
|
||||
"Choose icon": "Kies icoon",
|
||||
@@ -200,7 +210,6 @@
|
||||
"Unpublish": "Unpublish",
|
||||
"Permanently delete": "Permanently delete",
|
||||
"Move": "Verplaats",
|
||||
"History": "Geschiedenis",
|
||||
"Download": "Download",
|
||||
"Print": "Print",
|
||||
"Move {{ documentName }}": "Verplaats {{ documentName }}",
|
||||
@@ -552,5 +561,11 @@
|
||||
"Joined": "Lid geworden",
|
||||
"{{ time }} ago.": "{{ time }} geleden.",
|
||||
"Edit Profile": "Wijzig profiel",
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} heeft nog geen documenten gewijzigd."
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} heeft nog geen documenten gewijzigd.",
|
||||
"Today": "Today",
|
||||
"Yesterday": "Yesterday",
|
||||
"Last week": "Last week",
|
||||
"This month": "This month",
|
||||
"Last month": "Last month",
|
||||
"This year": "This year"
|
||||
}
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
"Drafts": "Kopie robocze",
|
||||
"Templates": "Szablony",
|
||||
"Deleted Collection": "Usunięta kolekcja",
|
||||
"New": "New",
|
||||
"History": "Historia",
|
||||
"Oh weird, there's nothing here": "To dziwne, ale nic tu nie ma",
|
||||
"New": "Nowe",
|
||||
"Only visible to you": "Widoczne tylko dla Ciebie",
|
||||
"Draft": "Szkic",
|
||||
"Template": "Szablon",
|
||||
@@ -110,6 +112,14 @@
|
||||
"our engineers have been notified": "our engineers have been notified",
|
||||
"Report a Bug": "Report a Bug",
|
||||
"Show Detail": "Show Detail",
|
||||
"Latest version": "Latest version",
|
||||
"{{userName}} edited": "{{userName}} edited",
|
||||
"{{userName}} archived": "{{userName}} archived",
|
||||
"{{userName}} restored": "{{userName}} restored",
|
||||
"{{userName}} deleted": "{{userName}} deleted",
|
||||
"{{userName}} moved from trash": "{{userName}} moved from trash",
|
||||
"{{userName}} published": "{{userName}} published",
|
||||
"{{userName}} moved": "{{userName}} moved",
|
||||
"Icon": "Icon",
|
||||
"Show menu": "Pokaż menu",
|
||||
"Choose icon": "Wybierz ikonę",
|
||||
@@ -200,7 +210,6 @@
|
||||
"Unpublish": "Cofnij publikację",
|
||||
"Permanently delete": "Permanently delete",
|
||||
"Move": "Przenieś",
|
||||
"History": "Historia",
|
||||
"Download": "Pobierz",
|
||||
"Print": "Drukuj",
|
||||
"Move {{ documentName }}": "Przenieś {{ documentName }}",
|
||||
@@ -486,17 +495,17 @@
|
||||
"<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",
|
||||
"Choose File": "Wybierz plik",
|
||||
"A full export might take some time, consider exporting a single document or collection if possible. We’ll 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. We’ll 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",
|
||||
"Document published": "Document published",
|
||||
"Receive a notification whenever a new document is published": "Receive a notification whenever a new document is published",
|
||||
"Document updated": "Document updated",
|
||||
"Receive a notification when a document you created is edited": "Receive a notification when a document you created is edited",
|
||||
"Collection created": "Collection created",
|
||||
"Receive a notification whenever a new collection is created": "Receive a notification whenever a new collection is created",
|
||||
"Export Data": "Eksportuj dane",
|
||||
"Document published": "Dokument opublikowany",
|
||||
"Receive a notification whenever a new document is published": "Otrzymuj powiadomienie o każdym nowym opublikowanym dokumencie",
|
||||
"Document updated": "Dokument zaktualizowany",
|
||||
"Receive a notification when a document you created is edited": "Otrzymuj powiadomienie o każdej modyfikacji utworzonego przez Ciebie dokumentu",
|
||||
"Collection created": "Kolekcja utworzona",
|
||||
"Receive a notification whenever a new collection is created": "Otrzymuj powiadomienie o każdym utworzeniu nowej kolekcji",
|
||||
"Getting started": "Getting started",
|
||||
"Tips on getting started with Outline`s features and functionality": "Tips on getting started with Outline`s features and functionality",
|
||||
"New features": "New features",
|
||||
@@ -543,14 +552,20 @@
|
||||
"Open Zapier": "Open Zapier",
|
||||
"You’ve not starred any documents yet.": "You’ve not starred any documents 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.",
|
||||
"You can create templates to help your team create consistent and accurate documentation.": "Możesz utworzyć szablony, aby ułatwić swojemu zespołowi tworzenie spójnej i dokładnej dokumentacji.",
|
||||
"Trash is empty at the moment.": "Kosz jest w tej chwili pusty.",
|
||||
"Are you sure? Deleting your account will destroy identifying data associated with your user and cannot be undone. You will be immediately logged out of Outline and all your API tokens will be revoked.": "Are you sure? Deleting your account will destroy identifying data associated with your user and cannot be undone. You will be immediately logged out of Outline and all your API tokens will be revoked.",
|
||||
"<em>Note:</em> Signing back in will cause a new account to be automatically reprovisioned.": "<em>Note:</em> Signing back in will cause a new account to be automatically reprovisioned.",
|
||||
"Delete My Account": "Delete My Account",
|
||||
"Delete My Account": "Usuń moje konto",
|
||||
"You joined": "Dołączyłeś",
|
||||
"Joined": "Dołączył",
|
||||
"{{ time }} ago.": "{{ time }} temu.",
|
||||
"Edit Profile": "Edit Profile",
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} nie zaktualizował jeszcze żadnych dokumentów."
|
||||
"Edit Profile": "Edytuj profil",
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} nie zaktualizował jeszcze żadnych dokumentów.",
|
||||
"Today": "Dzisiaj",
|
||||
"Yesterday": "Wczoraj",
|
||||
"Last week": "Last week",
|
||||
"This month": "This month",
|
||||
"Last month": "Last month",
|
||||
"This year": "This year"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"Drafts": "Rascunhos",
|
||||
"Templates": "Modelos",
|
||||
"Deleted Collection": "Excluir Coleção",
|
||||
"History": "Histórico",
|
||||
"Oh weird, there's nothing here": "Oh weird, there's nothing here",
|
||||
"New": "Novo",
|
||||
"Only visible to you": "Visível apenas para você",
|
||||
"Draft": "Rascunho",
|
||||
@@ -110,6 +112,14 @@
|
||||
"our engineers have been notified": "nossos engenheiros foram notificados",
|
||||
"Report a Bug": "Reportar um erro",
|
||||
"Show Detail": "Mostrar Detalhes",
|
||||
"Latest version": "Latest version",
|
||||
"{{userName}} edited": "{{userName}} edited",
|
||||
"{{userName}} archived": "{{userName}} archived",
|
||||
"{{userName}} restored": "{{userName}} restored",
|
||||
"{{userName}} deleted": "{{userName}} deleted",
|
||||
"{{userName}} moved from trash": "{{userName}} moved from trash",
|
||||
"{{userName}} published": "{{userName}} published",
|
||||
"{{userName}} moved": "{{userName}} moved",
|
||||
"Icon": "Ícone",
|
||||
"Show menu": "Mostrar menu",
|
||||
"Choose icon": "Escolha o ícone",
|
||||
@@ -200,7 +210,6 @@
|
||||
"Unpublish": "Cancelar publicação",
|
||||
"Permanently delete": "Apagar permanentemente",
|
||||
"Move": "Mover",
|
||||
"History": "Histórico",
|
||||
"Download": "Fazer download",
|
||||
"Print": "Imprimir",
|
||||
"Move {{ documentName }}": "Mover {{ documentName }}",
|
||||
@@ -552,5 +561,11 @@
|
||||
"Joined": "Entrou",
|
||||
"{{ time }} ago.": "{{ time }} atrás.",
|
||||
"Edit Profile": "Editar Perfil",
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} ainda não atualizou nenhum documento."
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} ainda não atualizou nenhum documento.",
|
||||
"Today": "Today",
|
||||
"Yesterday": "Yesterday",
|
||||
"Last week": "Last week",
|
||||
"This month": "This month",
|
||||
"Last month": "Last month",
|
||||
"This year": "This year"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"Drafts": "Rascunhos",
|
||||
"Templates": "Modelos",
|
||||
"Deleted Collection": "Coleção eliminada",
|
||||
"History": "Historia",
|
||||
"Oh weird, there's nothing here": "Oh weird, there's nothing here",
|
||||
"New": "Novo",
|
||||
"Only visible to you": "Apenas visível para ti",
|
||||
"Draft": "Rascunho",
|
||||
@@ -110,6 +112,14 @@
|
||||
"our engineers have been notified": "our engineers have been notified",
|
||||
"Report a Bug": "Report a Bug",
|
||||
"Show Detail": "Show Detail",
|
||||
"Latest version": "Latest version",
|
||||
"{{userName}} edited": "{{userName}} edited",
|
||||
"{{userName}} archived": "{{userName}} archived",
|
||||
"{{userName}} restored": "{{userName}} restored",
|
||||
"{{userName}} deleted": "{{userName}} deleted",
|
||||
"{{userName}} moved from trash": "{{userName}} moved from trash",
|
||||
"{{userName}} published": "{{userName}} published",
|
||||
"{{userName}} moved": "{{userName}} moved",
|
||||
"Icon": "Ícone",
|
||||
"Show menu": "Show menu",
|
||||
"Choose icon": "Choose icon",
|
||||
@@ -200,7 +210,6 @@
|
||||
"Unpublish": "Despublicar",
|
||||
"Permanently delete": "Permanently delete",
|
||||
"Move": "Mover",
|
||||
"History": "Historia",
|
||||
"Download": "Download",
|
||||
"Print": "Imprimir",
|
||||
"Move {{ documentName }}": "Move {{ documentName }}",
|
||||
@@ -552,5 +561,11 @@
|
||||
"Joined": "Entrou",
|
||||
"{{ time }} ago.": "{{ time }} atrás.",
|
||||
"Edit Profile": "Editar perfil",
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} ainda não atualizou nenhum documento."
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} ainda não atualizou nenhum documento.",
|
||||
"Today": "Today",
|
||||
"Yesterday": "Yesterday",
|
||||
"Last week": "Last week",
|
||||
"This month": "This month",
|
||||
"Last month": "Last month",
|
||||
"This year": "This year"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"Drafts": "Черновики",
|
||||
"Templates": "Шаблоны",
|
||||
"Deleted Collection": "Удаленная коллекция",
|
||||
"History": "История",
|
||||
"Oh weird, there's nothing here": "О странно, здесь ничего нет",
|
||||
"New": "Новое",
|
||||
"Only visible to you": "Видно только вам",
|
||||
"Draft": "Черновик",
|
||||
@@ -110,6 +112,14 @@
|
||||
"our engineers have been notified": "наши инженеры были уведомлены",
|
||||
"Report a Bug": "Сообщить об ошибке",
|
||||
"Show Detail": "Показать детали",
|
||||
"Latest version": "Последняя версия",
|
||||
"{{userName}} edited": "{{userName}} отредактировал",
|
||||
"{{userName}} archived": "{{userName}} archived",
|
||||
"{{userName}} restored": "{{userName}} restored",
|
||||
"{{userName}} deleted": "{{userName}} deleted",
|
||||
"{{userName}} moved from trash": "{{userName}} moved from trash",
|
||||
"{{userName}} published": "{{userName}} published",
|
||||
"{{userName}} moved": "{{userName}} moved",
|
||||
"Icon": "Значок",
|
||||
"Show menu": "Показать меню",
|
||||
"Choose icon": "Выбрать значок",
|
||||
@@ -200,7 +210,6 @@
|
||||
"Unpublish": "Снять с публикации",
|
||||
"Permanently delete": "Окончательно удалить",
|
||||
"Move": "Переместить",
|
||||
"History": "История",
|
||||
"Download": "Скачать",
|
||||
"Print": "Печать",
|
||||
"Move {{ documentName }}": "Переместить {{ documentName }}",
|
||||
@@ -381,8 +390,8 @@
|
||||
"Group members": "Участники группы",
|
||||
"Recently viewed": "Недавно просмотренные",
|
||||
"Created by me": "Созданные мной",
|
||||
"Weird, this shouldn’t ever be empty": "Weird, this shouldn’t ever be empty",
|
||||
"Documents you’ve recently viewed will be here for easy access": "Documents you’ve recently viewed will be here for easy access",
|
||||
"Weird, this shouldn’t ever be empty": "Думаю, это не должно быть пустым",
|
||||
"Documents you’ve recently viewed will be here for easy access": "Документы, которые вы недавно просматривали, будут здесь для быстрого доступа",
|
||||
"We sent out your invites!": "Мы отправили ваши приглашения!",
|
||||
"Sorry, you can only send {{MAX_INVITES}} invites at a time": "Извините, вы можете отправлять только {{MAX_INVITES}} за раз",
|
||||
"Invite team members or guests to join your knowledge base. Team members can sign in with {{signinMethods}} or use their email address.": "Пригласите членов команды или гостей присоединиться к вашей базе знаний. Члены команды могут войти в систему с использованием {{signinMethods}} или с помощью своего адреса электронной почты.",
|
||||
@@ -552,5 +561,11 @@
|
||||
"Joined": "Вступил",
|
||||
"{{ time }} ago.": "{{ time }} тому назад.",
|
||||
"Edit Profile": "Изменить профиль",
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} пока не изменял документы."
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} пока не изменял документы.",
|
||||
"Today": "Сегодня",
|
||||
"Yesterday": "Вчера",
|
||||
"Last week": "На прошлой неделе",
|
||||
"This month": "В этом месяце",
|
||||
"Last month": "Прошлый месяц",
|
||||
"This year": "В этом году"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"Drafts": "Utkast",
|
||||
"Templates": "Mallar",
|
||||
"Deleted Collection": "Deleted Collection",
|
||||
"History": "Historik",
|
||||
"Oh weird, there's nothing here": "Oh weird, there's nothing here",
|
||||
"New": "Ny",
|
||||
"Only visible to you": "Only visible to you",
|
||||
"Draft": "Utkast",
|
||||
@@ -110,6 +112,14 @@
|
||||
"our engineers have been notified": "our engineers have been notified",
|
||||
"Report a Bug": "Report a Bug",
|
||||
"Show Detail": "Show Detail",
|
||||
"Latest version": "Latest version",
|
||||
"{{userName}} edited": "{{userName}} edited",
|
||||
"{{userName}} archived": "{{userName}} archived",
|
||||
"{{userName}} restored": "{{userName}} restored",
|
||||
"{{userName}} deleted": "{{userName}} deleted",
|
||||
"{{userName}} moved from trash": "{{userName}} moved from trash",
|
||||
"{{userName}} published": "{{userName}} published",
|
||||
"{{userName}} moved": "{{userName}} moved",
|
||||
"Icon": "Ikon",
|
||||
"Show menu": "Show menu",
|
||||
"Choose icon": "Välj ikon",
|
||||
@@ -200,7 +210,6 @@
|
||||
"Unpublish": "Unpublish",
|
||||
"Permanently delete": "Permanently delete",
|
||||
"Move": "Flytta",
|
||||
"History": "Historik",
|
||||
"Download": "Ladda ned",
|
||||
"Print": "Skriv ut",
|
||||
"Move {{ documentName }}": "Move {{ documentName }}",
|
||||
@@ -552,5 +561,11 @@
|
||||
"Joined": "Joined",
|
||||
"{{ time }} ago.": "{{ time }} ago.",
|
||||
"Edit Profile": "Edit Profile",
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} hasn’t updated any documents yet."
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} hasn’t updated any documents yet.",
|
||||
"Today": "Today",
|
||||
"Yesterday": "Yesterday",
|
||||
"Last week": "Last week",
|
||||
"This month": "This month",
|
||||
"Last month": "Last month",
|
||||
"This year": "This year"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"Drafts": "ฉบับร่าง",
|
||||
"Templates": "Templates",
|
||||
"Deleted Collection": "Deleted Collection",
|
||||
"History": "History",
|
||||
"Oh weird, there's nothing here": "Oh weird, there's nothing here",
|
||||
"New": "สร้าง",
|
||||
"Only visible to you": "Only visible to you",
|
||||
"Draft": "ฉบับร่าง",
|
||||
@@ -110,6 +112,14 @@
|
||||
"our engineers have been notified": "our engineers have been notified",
|
||||
"Report a Bug": "Report a Bug",
|
||||
"Show Detail": "Show Detail",
|
||||
"Latest version": "Latest version",
|
||||
"{{userName}} edited": "{{userName}} edited",
|
||||
"{{userName}} archived": "{{userName}} archived",
|
||||
"{{userName}} restored": "{{userName}} restored",
|
||||
"{{userName}} deleted": "{{userName}} deleted",
|
||||
"{{userName}} moved from trash": "{{userName}} moved from trash",
|
||||
"{{userName}} published": "{{userName}} published",
|
||||
"{{userName}} moved": "{{userName}} moved",
|
||||
"Icon": "ไอคอน",
|
||||
"Show menu": "Show menu",
|
||||
"Choose icon": "เลือกไอคอน",
|
||||
@@ -200,7 +210,6 @@
|
||||
"Unpublish": "Unpublish",
|
||||
"Permanently delete": "Permanently delete",
|
||||
"Move": "Move",
|
||||
"History": "History",
|
||||
"Download": "Download",
|
||||
"Print": "Print",
|
||||
"Move {{ documentName }}": "Move {{ documentName }}",
|
||||
@@ -552,5 +561,11 @@
|
||||
"Joined": "Joined",
|
||||
"{{ time }} ago.": "{{ time }} ago.",
|
||||
"Edit Profile": "Edit Profile",
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} hasn’t updated any documents yet."
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} hasn’t updated any documents yet.",
|
||||
"Today": "Today",
|
||||
"Yesterday": "Yesterday",
|
||||
"Last week": "Last week",
|
||||
"This month": "This month",
|
||||
"Last month": "Last month",
|
||||
"This year": "This year"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"Drafts": "草稿箱",
|
||||
"Templates": "文档模板",
|
||||
"Deleted Collection": "删除文档集",
|
||||
"History": "历史记录",
|
||||
"Oh weird, there's nothing here": "Ohh,这里什么都没什么了",
|
||||
"New": "新",
|
||||
"Only visible to you": "只对您可见",
|
||||
"Draft": "草稿",
|
||||
@@ -34,11 +36,11 @@
|
||||
"only you": "仅您自己",
|
||||
"person": "人",
|
||||
"people": "用户",
|
||||
"{{ total }} task": "{{ total }} task",
|
||||
"{{ total }} task_plural": "{{ total }} tasks",
|
||||
"{{ completed }} task done": "{{ completed }} task done",
|
||||
"{{ completed }} task done_plural": "{{ completed }} tasks done",
|
||||
"{{ completed }} of {{ total }} tasks": "{{ completed }} of {{ total }} tasks",
|
||||
"{{ total }} task": "一共 {{ total }} 项任务",
|
||||
"{{ total }} task_plural": "一共 {{ total }} 项任务",
|
||||
"{{ completed }} task done": "已完成 {{ completed }} 项任务",
|
||||
"{{ completed }} task done_plural": "已完成 {{ completed }} 项任务",
|
||||
"{{ completed }} of {{ total }} tasks": "一共 {{ total }} 项任务,已完成 {{ completed }} 项",
|
||||
"Currently editing": "正在编辑",
|
||||
"Currently viewing": "正在浏览",
|
||||
"Viewed {{ timeAgo }} ago": "{{ timeAgo }} 前查看",
|
||||
@@ -50,7 +52,7 @@
|
||||
"Align left": "左对齐",
|
||||
"Align right": "右对齐",
|
||||
"Bulleted list": "无序列表",
|
||||
"Todo list": "Task list",
|
||||
"Todo list": "任务列表",
|
||||
"Code block": "代码块",
|
||||
"Copied to clipboard": "已复制到剪切板",
|
||||
"Code": "代码",
|
||||
@@ -75,7 +77,7 @@
|
||||
"Divider": "分割线",
|
||||
"Image": "图片",
|
||||
"Sorry, an error occurred uploading the image": "抱歉,上传图片时发生错误",
|
||||
"Write a caption": "Write a caption",
|
||||
"Write a caption": "撰写一个标题",
|
||||
"Info": "信息",
|
||||
"Info notice": "提示信息",
|
||||
"Link": "链接",
|
||||
@@ -101,20 +103,28 @@
|
||||
"Tip notice": "提示信息",
|
||||
"Warning": "警告",
|
||||
"Warning notice": "警告信息",
|
||||
"Module failed to load": "Module failed to load",
|
||||
"Loading Failed": "Loading Failed",
|
||||
"Sorry, part of the application failed to load. This may be because it was updated since you opened the tab or because of a failed network request. Please try reloading.": "Sorry, part of the application failed to load. This may be because it was updated since you opened the tab or because of a failed network request. Please try reloading.",
|
||||
"Reload": "Reload",
|
||||
"Something Unexpected Happened": "Something Unexpected Happened",
|
||||
"Sorry, an unrecoverable error occurred{{notified}}. Please try reloading the page, it may have been a temporary glitch.": "Sorry, an unrecoverable error occurred{{notified}}. Please try reloading the page, it may have been a temporary glitch.",
|
||||
"our engineers have been notified": "our engineers have been notified",
|
||||
"Report a Bug": "Report a Bug",
|
||||
"Show Detail": "Show Detail",
|
||||
"Module failed to load": "模块加载失败",
|
||||
"Loading Failed": "加载失败",
|
||||
"Sorry, part of the application failed to load. This may be because it was updated since you opened the tab or because of a failed network request. Please try reloading.": "抱歉,应用程序有一部分加载失败。 这可能是因为在您打开标签后已经更新,或者因为网络请求失败。请尝试重新加载。",
|
||||
"Reload": "重新加载",
|
||||
"Something Unexpected Happened": "发生了一些意料之外的错误",
|
||||
"Sorry, an unrecoverable error occurred{{notified}}. Please try reloading the page, it may have been a temporary glitch.": "抱歉,发生了一个无法恢复的错误{{notified}}。请尝试重新加载页面,这可能是一个暂时的小故障。",
|
||||
"our engineers have been notified": "我们的工程师已经收到通知",
|
||||
"Report a Bug": "反馈一个问题",
|
||||
"Show Detail": "显示详情",
|
||||
"Latest version": "最新版本",
|
||||
"{{userName}} edited": "已被 {{userName}} 编辑",
|
||||
"{{userName}} archived": "已被 {{userName}} 归档",
|
||||
"{{userName}} restored": "已被 {{userName}} 恢复",
|
||||
"{{userName}} deleted": "已被 {{userName}} 删除",
|
||||
"{{userName}} moved from trash": "已被 {{userName}} 已从回收站移出",
|
||||
"{{userName}} published": "已被 {{userName}} 发布",
|
||||
"{{userName}} moved": "已被 {{userName}} 移动",
|
||||
"Icon": "图标",
|
||||
"Show menu": "显示菜单",
|
||||
"Choose icon": "选择图标",
|
||||
"Loading": "加载中",
|
||||
"Loading editor": "Loading editor",
|
||||
"Loading editor": "正在加载编辑器",
|
||||
"Search": "搜索",
|
||||
"Default access": "默认访问",
|
||||
"View and edit": "查看并编辑",
|
||||
@@ -200,7 +210,6 @@
|
||||
"Unpublish": "取消发布",
|
||||
"Permanently delete": "永久删除",
|
||||
"Move": "移动",
|
||||
"History": "历史记录",
|
||||
"Download": "下载",
|
||||
"Print": "打印",
|
||||
"Move {{ documentName }}": "移动 {{ documentName }}",
|
||||
@@ -222,8 +231,8 @@
|
||||
"Share options": "分享选项",
|
||||
"Go to document": "转到文档",
|
||||
"Revoke link": "撤消链接",
|
||||
"Contents": "Contents",
|
||||
"Headings you add to the document will appear here": "Headings you add to the document will appear here",
|
||||
"Contents": "目录",
|
||||
"Headings you add to the document will appear here": "您添加到文档的标题将显示在此处",
|
||||
"Table of contents": "目录",
|
||||
"By {{ author }}": "创建人 {{ author }}",
|
||||
"Are you sure you want to make {{ userName }} an admin? Admins can modify team and billing information.": "您确定要设置 {{ userName }} 为管理员吗?管理员可以修改团队和帐单信息。",
|
||||
@@ -254,7 +263,7 @@
|
||||
"Least recently updated": "最近最少更新",
|
||||
"A–Z": "A-Z",
|
||||
"Drop documents to import": "拖放文档以导入",
|
||||
"Are you sure about that? Deleting the <em>{{collectionName}}</em> collection is permanent and cannot be restored, however documents within will be moved to the trash.": "Are you sure about that? Deleting the <em>{{collectionName}}</em> collection is permanent and cannot be restored, however documents within will be moved to the trash.",
|
||||
"Are you sure about that? Deleting the <em>{{collectionName}}</em> collection is permanent and cannot be restored, however documents within will be moved to the trash.": "您确定要这样做吗? 删除 <em>{{collectionName}}</em> 文档集是永久性的,且无法还原。不过,文档集中的文件将被移动到回收站。",
|
||||
"Deleting": "正在删除",
|
||||
"I’m sure – Delete": "确认删除",
|
||||
"The collection was updated": "文档集已更新",
|
||||
@@ -266,9 +275,9 @@
|
||||
"Public sharing is currently disabled in the team security settings.": "公共共享目前在团队安全设置中被禁用。",
|
||||
"Saving": "保存中",
|
||||
"Save": "保存",
|
||||
"Exporting the collection <em>{{collectionName}}</em> may take a few seconds. Your documents will be downloaded as a zip of folders with files in Markdown format.": "Exporting the collection <em>{{collectionName}}</em> may take a few seconds. Your documents will be downloaded as a zip of folders with files in Markdown format.",
|
||||
"Exporting": "Exporting",
|
||||
"Export Collection": "Export Collection",
|
||||
"Exporting the collection <em>{{collectionName}}</em> may take a few seconds. Your documents will be downloaded as a zip of folders with files in Markdown format.": "导出集合 <em>{{collectionName}}</em> 将会需要几秒钟。您的文档将会以 Markdown 格式放在文件夹中,并压缩成 zip 压缩包以供下载。",
|
||||
"Exporting": "正在导出……",
|
||||
"Export Collection": "导出文档集",
|
||||
"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": "正在创建",
|
||||
@@ -311,17 +320,17 @@
|
||||
"Add specific access for individual groups and team members": "为个别组和团队成员添加特定访问权限",
|
||||
"Add groups to {{ collectionName }}": "将群组加到 {{ collectionName }}",
|
||||
"Add people to {{ collectionName }}": "添加用户到 {{ collectionName }}",
|
||||
"Document updated by {{userName}}": "Document updated by {{userName}}",
|
||||
"You have unsaved changes.\nAre you sure you want to discard them?": "You have unsaved changes.\nAre you sure you want to discard them?",
|
||||
"Images are still uploading.\nAre you sure you want to discard them?": "Images are still uploading.\nAre you sure you want to discard them?",
|
||||
"You’re editing a template. Highlight some text and use the <2></2> control to add placeholders that can be filled out when creating new documents from this template.": "You’re editing a template. Highlight some text and use the <2></2> control to add placeholders that can be filled out when creating new documents from this template.",
|
||||
"Archived by {{userName}}": "Archived by {{userName}}",
|
||||
"Deleted by {{userName}}": "Deleted by {{userName}}",
|
||||
"This template will be permanently deleted in <2></2> unless restored.": "This template will be permanently deleted in <2></2> unless restored.",
|
||||
"This document will be permanently deleted in <2></2> unless restored.": "This document will be permanently deleted in <2></2> unless restored.",
|
||||
"Start your template…": "Start your template…",
|
||||
"Start with a title…": "Start with a title…",
|
||||
"…the rest is up to you": "…the rest is up to you",
|
||||
"Document updated by {{userName}}": "文档已被 {{userName}} 更新",
|
||||
"You have unsaved changes.\nAre you sure you want to discard them?": "您有尚未保存的修改。\n您确定要丢弃它们吗?",
|
||||
"Images are still uploading.\nAre you sure you want to discard them?": "图像仍在上传中。\n您确定要丢弃它们吗?",
|
||||
"You’re editing a template. Highlight some text and use the <2></2> control to add placeholders that can be filled out when creating new documents from this template.": "您正在编辑模板。突出显示一些文本并使用 <2></2>控件添加占位符,当从此模板创建新文档时可以填写这些占位符。",
|
||||
"Archived by {{userName}}": "已被 {{userName}} 存档",
|
||||
"Deleted by {{userName}}": "已被 {{userName}} 删除",
|
||||
"This template will be permanently deleted in <2></2> unless restored.": "此模板将在 <2></2> 中永久删除,除非手动恢复。",
|
||||
"This document will be permanently deleted in <2></2> unless restored.": "此文档将在 <2></2> 中永久删除,除非手动恢复。",
|
||||
"Start your template…": "开始创建模板…",
|
||||
"Start with a title…": "从撰写一个标题开始......",
|
||||
"…the rest is up to you": "…其余部分由您决定。",
|
||||
"Hide contents": "隐藏内容",
|
||||
"Show contents": "显示内容",
|
||||
"Edit {{noun}}": "编辑 {{noun}}",
|
||||
@@ -347,15 +356,15 @@
|
||||
"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 you’d like the option of referencing or restoring the {{noun}} in the future, consider archiving it instead.": "如果您将来希望引用或还原{{noun}},请考虑将其存档。",
|
||||
"Archiving": "正在归档",
|
||||
"Document moved": "Document moved",
|
||||
"Current location": "Current location",
|
||||
"Choose a new location": "Choose a new location",
|
||||
"Search collections & documents": "Search collections & documents",
|
||||
"Document moved": "文档已被移动",
|
||||
"Current location": "当前位置",
|
||||
"Choose a new location": "选择新的位置",
|
||||
"Search collections & documents": "搜索文档集和文档",
|
||||
"Couldn’t 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> 文档吗?此操作即时生效且无法撤消。",
|
||||
"Template created, go ahead and customize it": "Template created, go ahead and customize it",
|
||||
"Creating a template from <em>{{titleWithDefault}}</em> is a non-destructive action – we'll make a copy of the document and turn it into a template that can be used as a starting point for new documents.": "Creating a template from <em>{{titleWithDefault}}</em> is a non-destructive action – we'll make a copy of the document and turn it into a template that can be used as a starting point for new documents.",
|
||||
"Template created, go ahead and customize it": "模板已创建,继续进行自定义",
|
||||
"Creating a template from <em>{{titleWithDefault}}</em> is a non-destructive action – we'll make a copy of the document and turn it into a template that can be used as a starting point for new documents.": "从 <em>{{titleWithDefault}}</em> 创建模板是一个非破坏性的操作 - 我们将制作一个文档副本,并将其变成一个可以用作新文档起点的模板。",
|
||||
"Search documents": "搜索文档",
|
||||
"No documents found for your filters.": "没有找到相关文档。",
|
||||
"You’ve not got any drafts at the moment.": "您目前还没有任何草稿。",
|
||||
@@ -365,36 +374,36 @@
|
||||
"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>)已暂停您的帐户。要重新激活您的帐户,请直接与他们联系。",
|
||||
"Are you sure about that? Deleting the <em>{{groupName}}</em> group will cause its members to lose access to collections and documents that it is associated with.": "Are you sure about that? Deleting the <em>{{groupName}}</em> group will cause its members to lose access to collections and documents that it is associated with.",
|
||||
"You can edit the name of this group at any time, however doing so too often might confuse your team mates.": "You can edit the name of this group at any time, however doing so too often might confuse your team mates.",
|
||||
"Are you sure about that? Deleting the <em>{{groupName}}</em> group will cause its members to lose access to collections and documents that it is associated with.": "您确定要这样做吗? 删除 <em>{{groupName}}</em> 组将使其成员无法访问它所关联的文档集和文档。",
|
||||
"You can edit the name of this group at any time, however doing so too often might confuse your team mates.": "您可以随时编辑该群组的名称,但过于频繁可能会使您的队友感到困惑。",
|
||||
"{{userName}} was added to the group": "{{userName}} 已添加到群组",
|
||||
"Add team members below to give them access to the group. Need to add someone who’s not yet on the team yet?": "在下方添加团队成员以便让他们访问群组。需要添加尚未加入团队的人吗?",
|
||||
"Invite them to {{teamName}}": "邀请他们加入 {{teamName}}",
|
||||
"{{userName}} was removed from the group": "从群组中移除 {{userName}}。",
|
||||
"Add and remove team members in the <em>{{groupName}}</em> group. Adding people to the group will give them access to any collections this group has been added to.": "Add and remove team members in the <em>{{groupName}}</em> group. Adding people to the group will give them access to any collections this group has been added to.",
|
||||
"Listing team members in the <em>{{groupName}}</em> group.": "Listing team members in the <em>{{groupName}}</em> group.",
|
||||
"Add and remove team members in the <em>{{groupName}}</em> group. Adding people to the group will give them access to any collections this group has been added to.": "在 <em>{{groupName}}</em> 群组中添加和删除团队成员。 将人员添加到群组将使他们能够访问该群组添加到的所有文档集。",
|
||||
"Listing team members in the <em>{{groupName}}</em> group.": "将团队成员列入 <em>{{groupName}}</em> 组中。",
|
||||
"This group has no members.": "这个群组没有任何成员",
|
||||
"Add people to {{groupName}}": "Add people to {{groupName}}",
|
||||
"Groups are for organizing your team. They work best when centered around a function or a responsibility — Support or Engineering for example.": "Groups are for organizing your team. They work best when centered around a function or a responsibility — Support or Engineering for example.",
|
||||
"You’ll be able to add people to the group next.": "You’ll be able to add people to the group next.",
|
||||
"Continue": "Continue",
|
||||
"Group members": "Group members",
|
||||
"Add people to {{groupName}}": "添加用户到 {{groupName}}",
|
||||
"Groups are for organizing your team. They work best when centered around a function or a responsibility — Support or Engineering for example.": "群组用于管理您的团队。当以职能或责任为中心时,工作效果最好——例如支撑类或工程类。",
|
||||
"You’ll be able to add people to the group next.": "接下来,您将能够将人员添加到群组中。",
|
||||
"Continue": "继续操作",
|
||||
"Group members": "群组成员",
|
||||
"Recently viewed": "最近浏览",
|
||||
"Created by me": "由我创建",
|
||||
"Weird, this shouldn’t ever be empty": "Weird, this shouldn’t ever be empty",
|
||||
"Documents you’ve recently viewed will be here for easy access": "Documents you’ve recently viewed will be here for easy access",
|
||||
"We sent out your invites!": "We sent out your invites!",
|
||||
"Sorry, you can only send {{MAX_INVITES}} invites at a time": "Sorry, you can only send {{MAX_INVITES}} invites at a time",
|
||||
"Invite team members or guests to join your knowledge base. Team members can sign in with {{signinMethods}} or use their email address.": "Invite team members or guests to join your knowledge base. Team members can sign in with {{signinMethods}} or use their email address.",
|
||||
"Invite team members to join your knowledge base. They will need to sign in with {{signinMethods}}.": "Invite team members to join your knowledge base. They will need to sign in with {{signinMethods}}.",
|
||||
"As an admin you can also <2>enable email sign-in</2>.": "As an admin you can also <2>enable email sign-in</2>.",
|
||||
"Want a link to share directly with your team?": "Want a link to share directly with your team?",
|
||||
"Weird, this shouldn’t ever be empty": "奇怪,这里不应该是空的",
|
||||
"Documents you’ve recently viewed will be here for easy access": "您最近浏览过的文档将放在此处以方便快速访问",
|
||||
"We sent out your invites!": "我们发送了您的邀请!",
|
||||
"Sorry, you can only send {{MAX_INVITES}} invites at a time": "抱歉,您一次只能发送 {{MAX_INVITES}} 个邀请",
|
||||
"Invite team members or guests to join your knowledge base. Team members can sign in with {{signinMethods}} or use their email address.": "邀请团队成员或访客加入您的知识库。团队成员可以使用 {{signinMethods}} 登录或使用他们的电子邮件地址登录。",
|
||||
"Invite team members to join your knowledge base. They will need to sign in with {{signinMethods}}.": "邀请团队成员加入您的知识库。他们需要使用 {{signinMethods}} 登录。",
|
||||
"As an admin you can also <2>enable email sign-in</2>.": "作为管理员,您还可以<2>启用电子邮件登录</2>.",
|
||||
"Want a link to share directly with your team?": "想要一个链接直接分享到您的团队吗?",
|
||||
"Email": "邮箱",
|
||||
"Full name": "全名",
|
||||
"Remove invite": "Remove invite",
|
||||
"Add another": "Add another",
|
||||
"Inviting": "Inviting",
|
||||
"Send Invites": "Send Invites",
|
||||
"Remove invite": "撤消邀请",
|
||||
"Add another": "再添加一个",
|
||||
"Inviting": "邀请中……",
|
||||
"Send Invites": "发送邀请",
|
||||
"Navigation": "导航",
|
||||
"Edit current document": "编辑当前文档",
|
||||
"Move current document": "移动当前文档",
|
||||
@@ -424,19 +433,19 @@
|
||||
"Blockquote": "块引用",
|
||||
"Horizontal divider": "水平分隔线",
|
||||
"Inline code": "行内代码",
|
||||
"Back to home": "Back to home",
|
||||
"Back to website": "Back to website",
|
||||
"Check your email": "Check your email",
|
||||
"A magic sign-in link has been sent to the email <em>{{ emailLinkSentTo }}</em>, no password needed.": "A magic sign-in link has been sent to the email <em>{{ emailLinkSentTo }}</em>, no password needed.",
|
||||
"Back to login": "Back to login",
|
||||
"Create an account": "Create an account",
|
||||
"Get started by choosing a sign-in method for your new team below…": "Get started by choosing a sign-in method for your new team below…",
|
||||
"Login to {{ authProviderName }}": "Login to {{ authProviderName }}",
|
||||
"You signed in with {{ authProviderName }} last time.": "You signed in with {{ authProviderName }} last time.",
|
||||
"Already have an account? Go to <1>login</1>.": "Already have an account? Go to <1>login</1>.",
|
||||
"Sign In": "Sign In",
|
||||
"Continue with Email": "Continue with Email",
|
||||
"Continue with {{ authProviderName }}": "Continue with {{ authProviderName }}",
|
||||
"Back to home": "回到主页",
|
||||
"Back to website": "返回网站",
|
||||
"Check your email": "请查看您的电子邮件",
|
||||
"A magic sign-in link has been sent to the email <em>{{ emailLinkSentTo }}</em>, no password needed.": "魔法登录链接已经发送到电子邮箱 <em>{{ emailLinkSentTo }}</em>,不需要密码。",
|
||||
"Back to login": "回到登录界面",
|
||||
"Create an account": "创建一个帐户",
|
||||
"Get started by choosing a sign-in method for your new team below…": "请在下面为您的新团队选择一种登陆方式",
|
||||
"Login to {{ authProviderName }}": "登录到 {{ authProviderName }}",
|
||||
"You signed in with {{ authProviderName }} last time.": "您上次登录的方式为 {{ authProviderName }}。",
|
||||
"Already have an account? Go to <1>login</1>.": "已经有帐户了?前往<1>登录</1>.",
|
||||
"Sign In": "登录",
|
||||
"Continue with Email": "使用电子邮件继续",
|
||||
"Continue with {{ authProviderName }}": "使用 {{ authProviderName }} 继续",
|
||||
"Any collection": "文档集",
|
||||
"Any time": "任何时间",
|
||||
"Past day": "过去一天",
|
||||
@@ -466,14 +475,14 @@
|
||||
"Active": "活动的",
|
||||
"Everyone": "所有人",
|
||||
"Admins": "管理員",
|
||||
"Settings saved": "Settings saved",
|
||||
"Unable to upload new logo": "Unable to upload new logo",
|
||||
"These details affect the way that your Outline appears to everyone on the team.": "These details affect the way that your Outline appears to everyone on the team.",
|
||||
"Logo": "Logo",
|
||||
"Crop logo": "Crop logo",
|
||||
"Settings saved": "设置已保存",
|
||||
"Unable to upload new logo": "无法上传新logo",
|
||||
"These details affect the way that your Outline appears to everyone on the team.": "这些细节会影响Outline向团队中的每个人显示的方式。",
|
||||
"Logo": "网站标志 (Logo)",
|
||||
"Crop logo": "裁剪标志",
|
||||
"Upload": "上传",
|
||||
"Subdomain": "Subdomain",
|
||||
"Your knowledge base will be accessible at": "Your knowledge base will be accessible at",
|
||||
"Subdomain": "二级域名",
|
||||
"Your knowledge base will be accessible at": "您的知识库将在",
|
||||
"New group": "新建群组",
|
||||
"Groups can be used to organize and manage the people on your team.": "可以使用群组来组织和管理您团队中的人。",
|
||||
"All groups": "所有群组",
|
||||
@@ -491,20 +500,20 @@
|
||||
"Export Requested": "已请求导出",
|
||||
"Requesting Export": "正在请求导出",
|
||||
"Export Data": "导出数据",
|
||||
"Document published": "Document published",
|
||||
"Receive a notification whenever a new document is published": "Receive a notification whenever a new document is published",
|
||||
"Document updated": "Document updated",
|
||||
"Receive a notification when a document you created is edited": "Receive a notification when a document you created is edited",
|
||||
"Collection created": "Collection created",
|
||||
"Receive a notification whenever a new collection is created": "Receive a notification whenever a new collection is created",
|
||||
"Getting started": "Getting started",
|
||||
"Tips on getting started with Outline`s features and functionality": "Tips on getting started with Outline`s features and functionality",
|
||||
"New features": "New features",
|
||||
"Receive an email when new features of note are added": "Receive an email when new features of note are added",
|
||||
"Notifications saved": "Notifications saved",
|
||||
"Unsubscription successful. Your notification settings were updated": "Unsubscription successful. Your notification settings were updated",
|
||||
"Manage when and where you receive email notifications from Outline. Your email address can be updated in your SSO provider.": "Manage when and where you receive email notifications from Outline. Your email address can be updated in your SSO provider.",
|
||||
"Email address": "Email address",
|
||||
"Document published": "文档已发布",
|
||||
"Receive a notification whenever a new document is published": "每当发布新文档时收到通知",
|
||||
"Document updated": "文档已更新",
|
||||
"Receive a notification when a document you created is edited": "每当您创建的文档被编辑时收到通知",
|
||||
"Collection created": "已创建文档集",
|
||||
"Receive a notification whenever a new collection is created": "每当创建新文档集时收到通知",
|
||||
"Getting started": "新手指南",
|
||||
"Tips on getting started with Outline`s features and functionality": "开始使用 Outline 的特性和功能的小提示",
|
||||
"New features": "新特性",
|
||||
"Receive an email when new features of note are added": "当笔记增加新功能时收到电子邮件",
|
||||
"Notifications saved": "通知配置已保存",
|
||||
"Unsubscription successful. Your notification settings were updated": "取消订阅成功。您的通知设置已更新",
|
||||
"Manage when and where you receive email notifications from Outline. Your email address can be updated in your SSO provider.": "管理何时和何处收到Outline发送的电子邮件通知。您的电子邮件地址可以在您的 SSO 提供商中更新。",
|
||||
"Email address": "电子邮件地址",
|
||||
"Everyone that has signed into Outline appears here. It’s possible that there are other users who have access through {team.signinMethods} but haven’t signed in yet.": "每个登录到大纲的用户都会出现在这里。可能还有其他尚未登录的用户可以通过 {team.signinMethods} 访问。",
|
||||
"Filter": "筛选",
|
||||
"Profile saved": "配置已保存",
|
||||
@@ -516,12 +525,12 @@
|
||||
"Delete Account": "删除帐户",
|
||||
"You may delete your account at any time, note that this is unrecoverable": "您可以随时删除您的帐户,请注意删除后将无法恢复该账号",
|
||||
"Delete account": "删除账户",
|
||||
"Settings that impact the access, security, and content of your knowledge base.": "Settings that impact the access, security, and content of your knowledge base.",
|
||||
"Allow email authentication": "Allow email authentication",
|
||||
"When enabled, users can sign-in using their email address": "When enabled, users can sign-in using their email address",
|
||||
"When enabled, documents can be shared publicly on the internet by any team member": "When enabled, documents can be shared publicly on the internet by any team member",
|
||||
"Rich service embeds": "Rich service embeds",
|
||||
"Links to supported services are shown as rich embeds within your documents": "Links to supported services are shown as rich embeds within your documents",
|
||||
"Settings that impact the access, security, and content of your knowledge base.": "影响您知识库访问权限、安全性和内容的设置。",
|
||||
"Allow email authentication": "允许电子邮件身份验证",
|
||||
"When enabled, users can sign-in using their email address": "启用后,用户可以使用他们的电子邮件地址登录",
|
||||
"When enabled, documents can be shared publicly on the internet by any team member": "启用后,任何团队成员都可以在互联网上公开分享文档",
|
||||
"Rich service embeds": "丰富的服务嵌入",
|
||||
"Links to supported services are shown as rich embeds within your documents": "支持服务的链接作为丰富的嵌入显示在您的文档中",
|
||||
"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> 中对共享文档功能进行全局启用或禁用。",
|
||||
@@ -539,18 +548,24 @@
|
||||
"Tokens": "令牌",
|
||||
"Create a token": "创建令牌",
|
||||
"Zapier": "Zapier",
|
||||
"Zapier is a platform that allows Outline to easily integrate with thousands of other business tools. Head over to Zapier to setup a \"Zap\" and start programmatically interacting with Outline.'": "Zapier is a platform that allows Outline to easily integrate with thousands of other business tools. Head over to Zapier to setup a \"Zap\" and start programmatically interacting with Outline.'",
|
||||
"Open Zapier": "Open Zapier",
|
||||
"Zapier is a platform that allows Outline to easily integrate with thousands of other business tools. Head over to Zapier to setup a \"Zap\" and start programmatically interacting with Outline.'": "Zapier是一个平台,可以让Outline轻松地与数千个其他商业工具进行整合。 到 Zapier 来设置一个“Zap”,并开始程序性地与Outline交互。",
|
||||
"Open Zapier": "打开 Zapier",
|
||||
"You’ve 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.": "回收站为空。",
|
||||
"Are you sure? Deleting your account will destroy identifying data associated with your user and cannot be undone. You will be immediately logged out of Outline and all your API tokens will be revoked.": "Are you sure? Deleting your account will destroy identifying data associated with your user and cannot be undone. You will be immediately logged out of Outline and all your API tokens will be revoked.",
|
||||
"<em>Note:</em> Signing back in will cause a new account to be automatically reprovisioned.": "<em>Note:</em> Signing back in will cause a new account to be automatically reprovisioned.",
|
||||
"Delete My Account": "Delete My Account",
|
||||
"Are you sure? Deleting your account will destroy identifying data associated with your user and cannot be undone. You will be immediately logged out of Outline and all your API tokens will be revoked.": "你确定吗?注销您的帐户将销毁与您的用户相关联的识别数据并且无法撤消。您将立即退出 Outline,并且您的所有 API 令牌都将被撤销。",
|
||||
"<em>Note:</em> Signing back in will cause a new account to be automatically reprovisioned.": "<em>注意:</em> 重新登录将导致自动重新配置新帐户。",
|
||||
"Delete My Account": "注销我的帐户",
|
||||
"You joined": "您已加入",
|
||||
"Joined": "已加入",
|
||||
"{{ time }} ago.": "{{ time }} 之前。",
|
||||
"Edit Profile": "编辑个人资料",
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} 尚未更新任何文档。"
|
||||
"{{ userName }} hasn’t updated any documents yet.": "{{ userName }} 尚未更新任何文档。",
|
||||
"Today": "今天",
|
||||
"Yesterday": "昨天",
|
||||
"Last week": "上周",
|
||||
"This month": "本月",
|
||||
"Last month": "上月",
|
||||
"This year": "今年"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user