Move template management to settings (#5811)
This commit is contained in:
@@ -42,6 +42,7 @@ import {
|
|||||||
homePath,
|
homePath,
|
||||||
newDocumentPath,
|
newDocumentPath,
|
||||||
searchPath,
|
searchPath,
|
||||||
|
documentPath,
|
||||||
} from "~/utils/routeHelpers";
|
} from "~/utils/routeHelpers";
|
||||||
|
|
||||||
export const openDocument = createAction({
|
export const openDocument = createAction({
|
||||||
@@ -86,6 +87,48 @@ export const createDocument = createAction({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const createDocumentFromTemplate = createAction({
|
||||||
|
name: ({ t }) => t("New from template"),
|
||||||
|
analyticsName: "New document",
|
||||||
|
section: DocumentSection,
|
||||||
|
icon: <NewDocumentIcon />,
|
||||||
|
keywords: "create",
|
||||||
|
visible: ({ currentTeamId, activeDocumentId, stores }) =>
|
||||||
|
!!currentTeamId &&
|
||||||
|
!!activeDocumentId &&
|
||||||
|
stores.policies.abilities(currentTeamId).createDocument &&
|
||||||
|
!stores.documents.get(activeDocumentId)?.template,
|
||||||
|
perform: ({ activeCollectionId, activeDocumentId, inStarredSection }) =>
|
||||||
|
history.push(
|
||||||
|
newDocumentPath(activeCollectionId, { templateId: activeDocumentId }),
|
||||||
|
{
|
||||||
|
starred: inStarredSection,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createNestedDocument = createAction({
|
||||||
|
name: ({ t }) => t("New nested document"),
|
||||||
|
analyticsName: "New document",
|
||||||
|
section: DocumentSection,
|
||||||
|
icon: <NewDocumentIcon />,
|
||||||
|
keywords: "create",
|
||||||
|
visible: ({ currentTeamId, activeDocumentId, stores }) =>
|
||||||
|
!!currentTeamId &&
|
||||||
|
!!activeDocumentId &&
|
||||||
|
stores.policies.abilities(currentTeamId).createDocument &&
|
||||||
|
stores.policies.abilities(activeDocumentId).createChildDocument,
|
||||||
|
perform: ({ activeCollectionId, activeDocumentId, inStarredSection }) =>
|
||||||
|
history.push(
|
||||||
|
newDocumentPath(activeCollectionId, {
|
||||||
|
parentDocumentId: activeDocumentId,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
starred: inStarredSection,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
export const starDocument = createAction({
|
export const starDocument = createAction({
|
||||||
name: ({ t }) => t("Star"),
|
name: ({ t }) => t("Star"),
|
||||||
analyticsName: "Star document",
|
analyticsName: "Star document",
|
||||||
@@ -165,9 +208,14 @@ export const publishDocument = createAction({
|
|||||||
await document.save(undefined, {
|
await document.save(undefined, {
|
||||||
publish: true,
|
publish: true,
|
||||||
});
|
});
|
||||||
stores.toasts.showToast(t("Document published"), {
|
stores.toasts.showToast(
|
||||||
type: "success",
|
t("Published {{ documentName }}", {
|
||||||
});
|
documentName: document.noun,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
type: "success",
|
||||||
|
}
|
||||||
|
);
|
||||||
} else if (document) {
|
} else if (document) {
|
||||||
stores.dialogs.openModal({
|
stores.dialogs.openModal({
|
||||||
title: t("Publish document"),
|
title: t("Publish document"),
|
||||||
@@ -195,12 +243,20 @@ export const unpublishDocument = createAction({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const document = stores.documents.get(activeDocumentId);
|
const document = stores.documents.get(activeDocumentId);
|
||||||
|
if (!document) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await document?.unpublish();
|
await document.unpublish();
|
||||||
|
|
||||||
stores.toasts.showToast(t("Document unpublished"), {
|
stores.toasts.showToast(
|
||||||
type: "success",
|
t("Unpublished {{ documentName }}", {
|
||||||
});
|
documentName: document.noun,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
type: "success",
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -366,7 +422,7 @@ export const duplicateDocument = createAction({
|
|||||||
invariant(document, "Document must exist");
|
invariant(document, "Document must exist");
|
||||||
const duped = await document.duplicate();
|
const duped = await document.duplicate();
|
||||||
// when duplicating, go straight to the duplicated document content
|
// when duplicating, go straight to the duplicated document content
|
||||||
history.push(duped.url);
|
history.push(documentPath(duped));
|
||||||
stores.toasts.showToast(t("Document duplicated"), {
|
stores.toasts.showToast(t("Document duplicated"), {
|
||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
@@ -775,7 +831,16 @@ export const openDocumentInsights = createAction({
|
|||||||
icon: <LightBulbIcon />,
|
icon: <LightBulbIcon />,
|
||||||
visible: ({ activeDocumentId, stores }) => {
|
visible: ({ activeDocumentId, stores }) => {
|
||||||
const can = stores.policies.abilities(activeDocumentId ?? "");
|
const can = stores.policies.abilities(activeDocumentId ?? "");
|
||||||
return !!activeDocumentId && can.read;
|
const document = activeDocumentId
|
||||||
|
? stores.documents.get(activeDocumentId)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
!!activeDocumentId &&
|
||||||
|
can.read &&
|
||||||
|
!document?.isTemplate &&
|
||||||
|
!document?.isDeleted
|
||||||
|
);
|
||||||
},
|
},
|
||||||
perform: ({ activeDocumentId, stores }) => {
|
perform: ({ activeDocumentId, stores }) => {
|
||||||
if (!activeDocumentId) {
|
if (!activeDocumentId) {
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import {
|
|||||||
EditIcon,
|
EditIcon,
|
||||||
OpenIcon,
|
OpenIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
ShapesIcon,
|
|
||||||
KeyboardIcon,
|
KeyboardIcon,
|
||||||
EmailIcon,
|
EmailIcon,
|
||||||
LogoutIcon,
|
LogoutIcon,
|
||||||
ProfileIcon,
|
ProfileIcon,
|
||||||
BrowserIcon,
|
BrowserIcon,
|
||||||
|
ShapesIcon,
|
||||||
} from "outline-icons";
|
} from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { isMac } from "@shared/utils/browser";
|
import { isMac } from "@shared/utils/browser";
|
||||||
@@ -33,7 +33,6 @@ import {
|
|||||||
homePath,
|
homePath,
|
||||||
searchPath,
|
searchPath,
|
||||||
draftsPath,
|
draftsPath,
|
||||||
templatesPath,
|
|
||||||
archivePath,
|
archivePath,
|
||||||
trashPath,
|
trashPath,
|
||||||
settingsPath,
|
settingsPath,
|
||||||
@@ -67,15 +66,6 @@ export const navigateToDrafts = createAction({
|
|||||||
visible: ({ location }) => location.pathname !== draftsPath(),
|
visible: ({ location }) => location.pathname !== draftsPath(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const navigateToTemplates = createAction({
|
|
||||||
name: ({ t }) => t("Templates"),
|
|
||||||
analyticsName: "Navigate to templates",
|
|
||||||
section: NavigationSection,
|
|
||||||
icon: <ShapesIcon />,
|
|
||||||
perform: () => history.push(templatesPath()),
|
|
||||||
visible: ({ location }) => location.pathname !== templatesPath(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const navigateToArchive = createAction({
|
export const navigateToArchive = createAction({
|
||||||
name: ({ t }) => t("Archive"),
|
name: ({ t }) => t("Archive"),
|
||||||
analyticsName: "Navigate to archive",
|
analyticsName: "Navigate to archive",
|
||||||
@@ -103,7 +93,7 @@ export const navigateToSettings = createAction({
|
|||||||
icon: <SettingsIcon />,
|
icon: <SettingsIcon />,
|
||||||
visible: ({ stores }) =>
|
visible: ({ stores }) =>
|
||||||
stores.policies.abilities(stores.auth.team?.id || "").update,
|
stores.policies.abilities(stores.auth.team?.id || "").update,
|
||||||
perform: () => history.push(settingsPath("details")),
|
perform: () => history.push(settingsPath()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const navigateToProfileSettings = createAction({
|
export const navigateToProfileSettings = createAction({
|
||||||
@@ -115,6 +105,15 @@ export const navigateToProfileSettings = createAction({
|
|||||||
perform: () => history.push(settingsPath()),
|
perform: () => history.push(settingsPath()),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const navigateToTemplateSettings = createAction({
|
||||||
|
name: ({ t }) => t("Templates"),
|
||||||
|
analyticsName: "Navigate to template settings",
|
||||||
|
section: NavigationSection,
|
||||||
|
iconInContextMenu: false,
|
||||||
|
icon: <ShapesIcon />,
|
||||||
|
perform: () => history.push(settingsPath("templates")),
|
||||||
|
});
|
||||||
|
|
||||||
export const navigateToNotificationSettings = createAction({
|
export const navigateToNotificationSettings = createAction({
|
||||||
name: ({ t }) => t("Notifications"),
|
name: ({ t }) => t("Notifications"),
|
||||||
analyticsName: "Navigate to notification settings",
|
analyticsName: "Navigate to notification settings",
|
||||||
@@ -216,7 +215,6 @@ export const logout = createAction({
|
|||||||
export const rootNavigationActions = [
|
export const rootNavigationActions = [
|
||||||
navigateToHome,
|
navigateToHome,
|
||||||
navigateToDrafts,
|
navigateToDrafts,
|
||||||
navigateToTemplates,
|
|
||||||
navigateToArchive,
|
navigateToArchive,
|
||||||
navigateToTrash,
|
navigateToTrash,
|
||||||
downloadApp,
|
downloadApp,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { MenuInternalLink } from "~/types";
|
|||||||
import {
|
import {
|
||||||
archivePath,
|
archivePath,
|
||||||
collectionPath,
|
collectionPath,
|
||||||
templatesPath,
|
settingsPath,
|
||||||
trashPath,
|
trashPath,
|
||||||
} from "~/utils/routeHelpers";
|
} from "~/utils/routeHelpers";
|
||||||
import EmojiIcon from "./Icons/EmojiIcon";
|
import EmojiIcon from "./Icons/EmojiIcon";
|
||||||
@@ -44,12 +44,12 @@ function useCategory(document: Document): MenuInternalLink | null {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (document.isTemplate) {
|
if (document.template) {
|
||||||
return {
|
return {
|
||||||
type: "route",
|
type: "route",
|
||||||
icon: <ShapesIcon />,
|
icon: <ShapesIcon />,
|
||||||
title: t("Templates"),
|
title: t("Templates"),
|
||||||
to: templatesPath(),
|
to: settingsPath("templates"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { PlusIcon } from "outline-icons";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
@@ -9,7 +8,6 @@ import breakpoint from "styled-components-breakpoint";
|
|||||||
import { s } from "@shared/styles";
|
import { s } from "@shared/styles";
|
||||||
import Document from "~/models/Document";
|
import Document from "~/models/Document";
|
||||||
import Badge from "~/components/Badge";
|
import Badge from "~/components/Badge";
|
||||||
import Button from "~/components/Button";
|
|
||||||
import DocumentMeta from "~/components/DocumentMeta";
|
import DocumentMeta from "~/components/DocumentMeta";
|
||||||
import EventBoundary from "~/components/EventBoundary";
|
import EventBoundary from "~/components/EventBoundary";
|
||||||
import Flex from "~/components/Flex";
|
import Flex from "~/components/Flex";
|
||||||
@@ -18,12 +16,10 @@ import NudeButton from "~/components/NudeButton";
|
|||||||
import StarButton, { AnimatedStar } from "~/components/Star";
|
import StarButton, { AnimatedStar } from "~/components/Star";
|
||||||
import Tooltip from "~/components/Tooltip";
|
import Tooltip from "~/components/Tooltip";
|
||||||
import useBoolean from "~/hooks/useBoolean";
|
import useBoolean from "~/hooks/useBoolean";
|
||||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
|
||||||
import useCurrentUser from "~/hooks/useCurrentUser";
|
import useCurrentUser from "~/hooks/useCurrentUser";
|
||||||
import usePolicy from "~/hooks/usePolicy";
|
|
||||||
import DocumentMenu from "~/menus/DocumentMenu";
|
import DocumentMenu from "~/menus/DocumentMenu";
|
||||||
import { hover } from "~/styles";
|
import { hover } from "~/styles";
|
||||||
import { newDocumentPath } from "~/utils/routeHelpers";
|
import { documentPath } from "~/utils/routeHelpers";
|
||||||
import EmojiIcon from "./Icons/EmojiIcon";
|
import EmojiIcon from "./Icons/EmojiIcon";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -52,7 +48,6 @@ function DocumentListItem(
|
|||||||
) {
|
) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const user = useCurrentUser();
|
const user = useCurrentUser();
|
||||||
const team = useCurrentTeam();
|
|
||||||
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
|
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -72,8 +67,6 @@ function DocumentListItem(
|
|||||||
!!document.title.toLowerCase().includes(highlight.toLowerCase());
|
!!document.title.toLowerCase().includes(highlight.toLowerCase());
|
||||||
const canStar =
|
const canStar =
|
||||||
!document.isDraft && !document.isArchived && !document.isTemplate;
|
!document.isDraft && !document.isArchived && !document.isTemplate;
|
||||||
const can = usePolicy(team);
|
|
||||||
const canCollection = usePolicy(document.collectionId);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CompositeItem
|
<CompositeItem
|
||||||
@@ -84,7 +77,7 @@ function DocumentListItem(
|
|||||||
$isStarred={document.isStarred}
|
$isStarred={document.isStarred}
|
||||||
$menuOpen={menuOpen}
|
$menuOpen={menuOpen}
|
||||||
to={{
|
to={{
|
||||||
pathname: document.url,
|
pathname: documentPath(document),
|
||||||
state: {
|
state: {
|
||||||
title: document.titleWithDefault,
|
title: document.titleWithDefault,
|
||||||
},
|
},
|
||||||
@@ -142,25 +135,6 @@ function DocumentListItem(
|
|||||||
/>
|
/>
|
||||||
</Content>
|
</Content>
|
||||||
<Actions>
|
<Actions>
|
||||||
{document.isTemplate &&
|
|
||||||
!document.isArchived &&
|
|
||||||
!document.isDeleted &&
|
|
||||||
can.createDocument &&
|
|
||||||
canCollection.update && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
as={Link}
|
|
||||||
to={newDocumentPath(document.collectionId, {
|
|
||||||
templateId: document.id,
|
|
||||||
})}
|
|
||||||
icon={<PlusIcon />}
|
|
||||||
neutral
|
|
||||||
>
|
|
||||||
{t("New doc")}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<DocumentMenu
|
<DocumentMenu
|
||||||
document={document}
|
document={document}
|
||||||
showPin={showPin}
|
showPin={showPin}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LocationDescriptor } from "history";
|
import { LocationDescriptor, LocationDescriptorObject } from "history";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { match, NavLink, Route } from "react-router-dom";
|
import { match, NavLink, Route } from "react-router-dom";
|
||||||
|
|
||||||
@@ -9,10 +9,20 @@ type Props = React.ComponentProps<typeof NavLink> & {
|
|||||||
[x: string]: string | undefined;
|
[x: string]: string | undefined;
|
||||||
}>
|
}>
|
||||||
| boolean
|
| boolean
|
||||||
| null
|
| null,
|
||||||
|
location: LocationDescriptorObject
|
||||||
) => React.ReactNode;
|
) => React.ReactNode;
|
||||||
|
/**
|
||||||
|
* If true, the tab will only be active if the path matches exactly.
|
||||||
|
*/
|
||||||
exact?: boolean;
|
exact?: boolean;
|
||||||
|
/**
|
||||||
|
* CSS properties to apply to the link when it is active.
|
||||||
|
*/
|
||||||
activeStyle?: React.CSSProperties;
|
activeStyle?: React.CSSProperties;
|
||||||
|
/**
|
||||||
|
* The path to match against the current location.
|
||||||
|
*/
|
||||||
to: LocationDescriptor;
|
to: LocationDescriptor;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -25,7 +35,10 @@ function NavLinkWithChildrenFunc(
|
|||||||
{({ match, location }) => (
|
{({ match, location }) => (
|
||||||
<NavLink {...rest} to={to} exact={exact} ref={ref}>
|
<NavLink {...rest} to={to} exact={exact} ref={ref}>
|
||||||
{children
|
{children
|
||||||
? children(rest.isActive ? rest.isActive(match, location) : match)
|
? children(
|
||||||
|
rest.isActive ? rest.isActive(match, location) : match,
|
||||||
|
location
|
||||||
|
)
|
||||||
: null}
|
: null}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import {
|
import { EditIcon, SearchIcon, HomeIcon, SidebarIcon } from "outline-icons";
|
||||||
EditIcon,
|
|
||||||
SearchIcon,
|
|
||||||
ShapesIcon,
|
|
||||||
HomeIcon,
|
|
||||||
SidebarIcon,
|
|
||||||
} from "outline-icons";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { DndProvider } from "react-dnd";
|
import { DndProvider } from "react-dnd";
|
||||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||||
@@ -21,12 +15,7 @@ import usePolicy from "~/hooks/usePolicy";
|
|||||||
import useStores from "~/hooks/useStores";
|
import useStores from "~/hooks/useStores";
|
||||||
import OrganizationMenu from "~/menus/OrganizationMenu";
|
import OrganizationMenu from "~/menus/OrganizationMenu";
|
||||||
import { metaDisplay } from "~/utils/keyboard";
|
import { metaDisplay } from "~/utils/keyboard";
|
||||||
import {
|
import { homePath, draftsPath, searchPath } from "~/utils/routeHelpers";
|
||||||
homePath,
|
|
||||||
draftsPath,
|
|
||||||
templatesPath,
|
|
||||||
searchPath,
|
|
||||||
} from "~/utils/routeHelpers";
|
|
||||||
import TeamLogo from "../TeamLogo";
|
import TeamLogo from "../TeamLogo";
|
||||||
import Tooltip from "../Tooltip";
|
import Tooltip from "../Tooltip";
|
||||||
import Sidebar from "./Sidebar";
|
import Sidebar from "./Sidebar";
|
||||||
@@ -52,7 +41,6 @@ function AppSidebar() {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!user.isViewer) {
|
if (!user.isViewer) {
|
||||||
void documents.fetchDrafts();
|
void documents.fetchDrafts();
|
||||||
void documents.fetchTemplates();
|
|
||||||
}
|
}
|
||||||
}, [documents, user.isViewer]);
|
}, [documents, user.isViewer]);
|
||||||
|
|
||||||
@@ -138,19 +126,6 @@ function AppSidebar() {
|
|||||||
<Section>
|
<Section>
|
||||||
{can.createDocument && (
|
{can.createDocument && (
|
||||||
<>
|
<>
|
||||||
<SidebarLink
|
|
||||||
to={templatesPath()}
|
|
||||||
icon={<ShapesIcon />}
|
|
||||||
exact={false}
|
|
||||||
label={t("Templates")}
|
|
||||||
active={
|
|
||||||
documents.active
|
|
||||||
? documents.active.isTemplate &&
|
|
||||||
!documents.active.isDeleted &&
|
|
||||||
!documents.active.isArchived
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ArchiveLink />
|
<ArchiveLink />
|
||||||
<TrashLink />
|
<TrashLink />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { observer } from "mobx-react";
|
|||||||
import { BackIcon, SidebarIcon } from "outline-icons";
|
import { BackIcon, SidebarIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import Flex from "~/components/Flex";
|
import Flex from "~/components/Flex";
|
||||||
import Scrollable from "~/components/Scrollable";
|
import Scrollable from "~/components/Scrollable";
|
||||||
@@ -11,6 +11,7 @@ import useSettingsConfig from "~/hooks/useSettingsConfig";
|
|||||||
import useStores from "~/hooks/useStores";
|
import useStores from "~/hooks/useStores";
|
||||||
import isCloudHosted from "~/utils/isCloudHosted";
|
import isCloudHosted from "~/utils/isCloudHosted";
|
||||||
import { metaDisplay } from "~/utils/keyboard";
|
import { metaDisplay } from "~/utils/keyboard";
|
||||||
|
import { settingsPath } from "~/utils/routeHelpers";
|
||||||
import Tooltip from "../Tooltip";
|
import Tooltip from "../Tooltip";
|
||||||
import Sidebar from "./Sidebar";
|
import Sidebar from "./Sidebar";
|
||||||
import Header from "./components/Header";
|
import Header from "./components/Header";
|
||||||
@@ -25,6 +26,7 @@ function SettingsSidebar() {
|
|||||||
const { ui } = useStores();
|
const { ui } = useStores();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const location = useLocation();
|
||||||
const configs = useSettingsConfig();
|
const configs = useSettingsConfig();
|
||||||
const groupedConfig = groupBy(configs, "group");
|
const groupedConfig = groupBy(configs, "group");
|
||||||
|
|
||||||
@@ -62,6 +64,11 @@ function SettingsSidebar() {
|
|||||||
<SidebarLink
|
<SidebarLink
|
||||||
key={item.path}
|
key={item.path}
|
||||||
to={item.path}
|
to={item.path}
|
||||||
|
active={
|
||||||
|
item.path !== settingsPath()
|
||||||
|
? location.pathname.startsWith(item.path)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
icon={<item.icon />}
|
icon={<item.icon />}
|
||||||
label={item.name}
|
label={item.name}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { m } from "framer-motion";
|
import { m } from "framer-motion";
|
||||||
|
import { LocationDescriptor } from "history";
|
||||||
|
import isEqual from "lodash/isEqual";
|
||||||
|
import queryString from "query-string";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import styled, { useTheme } from "styled-components";
|
import styled, { useTheme } from "styled-components";
|
||||||
import { s } from "@shared/styles";
|
import { s } from "@shared/styles";
|
||||||
@@ -6,8 +9,19 @@ import NavLink from "~/components/NavLink";
|
|||||||
import { hover } from "~/styles";
|
import { hover } from "~/styles";
|
||||||
|
|
||||||
type Props = Omit<React.ComponentProps<typeof NavLink>, "children"> & {
|
type Props = Omit<React.ComponentProps<typeof NavLink>, "children"> & {
|
||||||
to: string;
|
/**
|
||||||
|
* The path to match against the current location.
|
||||||
|
*/
|
||||||
|
to: LocationDescriptor;
|
||||||
|
/**
|
||||||
|
* If true, the tab will only be active if the path matches exactly.
|
||||||
|
*/
|
||||||
exact?: boolean;
|
exact?: boolean;
|
||||||
|
/**
|
||||||
|
* If true, the tab will only be active if the query string matches exactly.
|
||||||
|
* By default query string parameters are ignored for location mathing.
|
||||||
|
*/
|
||||||
|
exactQueryString?: boolean;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,24 +59,38 @@ const transition = {
|
|||||||
damping: 30,
|
damping: 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Tab: React.FC<Props> = ({ children, ...rest }: Props) => {
|
const Tab: React.FC<Props> = ({
|
||||||
|
children,
|
||||||
|
exact,
|
||||||
|
exactQueryString,
|
||||||
|
...rest
|
||||||
|
}: Props) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const activeStyle = {
|
const activeStyle = {
|
||||||
color: theme.textSecondary,
|
color: theme.textSecondary,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabLink {...rest} activeStyle={activeStyle}>
|
<TabLink
|
||||||
{(match) => (
|
{...rest}
|
||||||
|
exact={exact || exactQueryString}
|
||||||
|
activeStyle={activeStyle}
|
||||||
|
>
|
||||||
|
{(match, location) => (
|
||||||
<>
|
<>
|
||||||
{children}
|
{children}
|
||||||
{match && (
|
{match &&
|
||||||
<Active
|
(!exactQueryString ||
|
||||||
layoutId="underline"
|
isEqual(
|
||||||
initial={false}
|
queryString.parse(location.search ?? ""),
|
||||||
transition={transition}
|
queryString.parse(rest.to.search as string)
|
||||||
/>
|
)) && (
|
||||||
)}
|
<Active
|
||||||
|
layoutId="underline"
|
||||||
|
initial={false}
|
||||||
|
transition={transition}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</TabLink>
|
</TabLink>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import useStores from "~/hooks/useStores";
|
import useStores from "~/hooks/useStores";
|
||||||
import useToasts from "~/hooks/useToasts";
|
import useToasts from "~/hooks/useToasts";
|
||||||
|
import { documentPath } from "~/utils/routeHelpers";
|
||||||
|
|
||||||
let importingLock = false;
|
let importingLock = false;
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ export default function useImportDocument(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (redirect) {
|
if (redirect) {
|
||||||
history.push(doc.url);
|
history.push(documentPath(doc));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -12,38 +12,43 @@ import {
|
|||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
ExportIcon,
|
ExportIcon,
|
||||||
ImportIcon,
|
ImportIcon,
|
||||||
|
ShapesIcon,
|
||||||
|
Icon,
|
||||||
} from "outline-icons";
|
} from "outline-icons";
|
||||||
import React from "react";
|
import React, { ComponentProps } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { integrationSettingsPath } from "@shared/utils/routeHelpers";
|
import { integrationSettingsPath } from "@shared/utils/routeHelpers";
|
||||||
import ApiKeys from "~/scenes/Settings/ApiKeys";
|
|
||||||
import Details from "~/scenes/Settings/Details";
|
|
||||||
import Export from "~/scenes/Settings/Export";
|
|
||||||
import Features from "~/scenes/Settings/Features";
|
|
||||||
import GoogleAnalytics from "~/scenes/Settings/GoogleAnalytics";
|
|
||||||
import Groups from "~/scenes/Settings/Groups";
|
|
||||||
import Import from "~/scenes/Settings/Import";
|
|
||||||
import Members from "~/scenes/Settings/Members";
|
|
||||||
import Notifications from "~/scenes/Settings/Notifications";
|
|
||||||
import Preferences from "~/scenes/Settings/Preferences";
|
|
||||||
import Profile from "~/scenes/Settings/Profile";
|
|
||||||
import Security from "~/scenes/Settings/Security";
|
|
||||||
import SelfHosted from "~/scenes/Settings/SelfHosted";
|
|
||||||
import Shares from "~/scenes/Settings/Shares";
|
|
||||||
import Zapier from "~/scenes/Settings/Zapier";
|
|
||||||
import GoogleIcon from "~/components/Icons/GoogleIcon";
|
import GoogleIcon from "~/components/Icons/GoogleIcon";
|
||||||
import ZapierIcon from "~/components/Icons/ZapierIcon";
|
import ZapierIcon from "~/components/Icons/ZapierIcon";
|
||||||
import PluginLoader from "~/utils/PluginLoader";
|
import PluginLoader from "~/utils/PluginLoader";
|
||||||
import isCloudHosted from "~/utils/isCloudHosted";
|
import isCloudHosted from "~/utils/isCloudHosted";
|
||||||
|
import lazy from "~/utils/lazyWithRetry";
|
||||||
import { settingsPath } from "~/utils/routeHelpers";
|
import { settingsPath } from "~/utils/routeHelpers";
|
||||||
import useCurrentTeam from "./useCurrentTeam";
|
import useCurrentTeam from "./useCurrentTeam";
|
||||||
import usePolicy from "./usePolicy";
|
import usePolicy from "./usePolicy";
|
||||||
|
|
||||||
|
const ApiKeys = lazy(() => import("~/scenes/Settings/ApiKeys"));
|
||||||
|
const Details = lazy(() => import("~/scenes/Settings/Details"));
|
||||||
|
const Export = lazy(() => import("~/scenes/Settings/Export"));
|
||||||
|
const Features = lazy(() => import("~/scenes/Settings/Features"));
|
||||||
|
const GoogleAnalytics = lazy(() => import("~/scenes/Settings/GoogleAnalytics"));
|
||||||
|
const Groups = lazy(() => import("~/scenes/Settings/Groups"));
|
||||||
|
const Import = lazy(() => import("~/scenes/Settings/Import"));
|
||||||
|
const Members = lazy(() => import("~/scenes/Settings/Members"));
|
||||||
|
const Notifications = lazy(() => import("~/scenes/Settings/Notifications"));
|
||||||
|
const Preferences = lazy(() => import("~/scenes/Settings/Preferences"));
|
||||||
|
const Profile = lazy(() => import("~/scenes/Settings/Profile"));
|
||||||
|
const Security = lazy(() => import("~/scenes/Settings/Security"));
|
||||||
|
const SelfHosted = lazy(() => import("~/scenes/Settings/SelfHosted"));
|
||||||
|
const Shares = lazy(() => import("~/scenes/Settings/Shares"));
|
||||||
|
const Templates = lazy(() => import("~/scenes/Settings/Templates"));
|
||||||
|
const Zapier = lazy(() => import("~/scenes/Settings/Zapier"));
|
||||||
|
|
||||||
export type ConfigItem = {
|
export type ConfigItem = {
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
icon: React.FC<any>;
|
icon: React.FC<ComponentProps<typeof Icon>>;
|
||||||
component: React.ComponentType<any>;
|
component: React.ComponentType;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
group: string;
|
group: string;
|
||||||
};
|
};
|
||||||
@@ -55,6 +60,7 @@ const useSettingsConfig = () => {
|
|||||||
|
|
||||||
const config = React.useMemo(() => {
|
const config = React.useMemo(() => {
|
||||||
const items: ConfigItem[] = [
|
const items: ConfigItem[] = [
|
||||||
|
// Account
|
||||||
{
|
{
|
||||||
name: t("Profile"),
|
name: t("Profile"),
|
||||||
path: settingsPath(),
|
path: settingsPath(),
|
||||||
@@ -87,7 +93,7 @@ const useSettingsConfig = () => {
|
|||||||
group: t("Account"),
|
group: t("Account"),
|
||||||
icon: CodeIcon,
|
icon: CodeIcon,
|
||||||
},
|
},
|
||||||
// Team group
|
// Workspace
|
||||||
{
|
{
|
||||||
name: t("Details"),
|
name: t("Details"),
|
||||||
path: settingsPath("details"),
|
path: settingsPath("details"),
|
||||||
@@ -128,6 +134,14 @@ const useSettingsConfig = () => {
|
|||||||
group: t("Workspace"),
|
group: t("Workspace"),
|
||||||
icon: GroupIcon,
|
icon: GroupIcon,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: t("Templates"),
|
||||||
|
path: settingsPath("templates"),
|
||||||
|
component: Templates,
|
||||||
|
enabled: true,
|
||||||
|
group: t("Workspace"),
|
||||||
|
icon: ShapesIcon,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: t("Shared Links"),
|
name: t("Shared Links"),
|
||||||
path: settingsPath("shares"),
|
path: settingsPath("shares"),
|
||||||
@@ -152,6 +166,7 @@ const useSettingsConfig = () => {
|
|||||||
group: t("Workspace"),
|
group: t("Workspace"),
|
||||||
icon: ExportIcon,
|
icon: ExportIcon,
|
||||||
},
|
},
|
||||||
|
// Integrations
|
||||||
{
|
{
|
||||||
name: t("Self Hosted"),
|
name: t("Self Hosted"),
|
||||||
path: integrationSettingsPath("self-hosted"),
|
path: integrationSettingsPath("self-hosted"),
|
||||||
@@ -190,6 +205,7 @@ const useSettingsConfig = () => {
|
|||||||
const item = {
|
const item = {
|
||||||
name: t(plugin.config.name),
|
name: t(plugin.config.name),
|
||||||
path: integrationSettingsPath(plugin.id),
|
path: integrationSettingsPath(plugin.id),
|
||||||
|
// TODO: Remove hardcoding of plugin id here
|
||||||
group: plugin.id === "collections" ? t("Workspace") : t("Integrations"),
|
group: plugin.id === "collections" ? t("Workspace") : t("Integrations"),
|
||||||
component: plugin.settings,
|
component: plugin.settings,
|
||||||
enabled: enabledInDeployment && hasSettings && can.update,
|
enabled: enabledInDeployment && hasSettings && can.update,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { EditIcon, NewDocumentIcon, RestoreIcon } from "outline-icons";
|
import { EditIcon, RestoreIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
@@ -38,6 +38,8 @@ import {
|
|||||||
unpublishDocument,
|
unpublishDocument,
|
||||||
printDocument,
|
printDocument,
|
||||||
openDocumentComments,
|
openDocumentComments,
|
||||||
|
createDocumentFromTemplate,
|
||||||
|
createNestedDocument,
|
||||||
} from "~/actions/definitions/documents";
|
} from "~/actions/definitions/documents";
|
||||||
import useActionContext from "~/hooks/useActionContext";
|
import useActionContext from "~/hooks/useActionContext";
|
||||||
import useCurrentUser from "~/hooks/useCurrentUser";
|
import useCurrentUser from "~/hooks/useCurrentUser";
|
||||||
@@ -47,7 +49,7 @@ import useRequest from "~/hooks/useRequest";
|
|||||||
import useStores from "~/hooks/useStores";
|
import useStores from "~/hooks/useStores";
|
||||||
import useToasts from "~/hooks/useToasts";
|
import useToasts from "~/hooks/useToasts";
|
||||||
import { MenuItem } from "~/types";
|
import { MenuItem } from "~/types";
|
||||||
import { documentEditPath, newDocumentPath } from "~/utils/routeHelpers";
|
import { documentEditPath } from "~/utils/routeHelpers";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
document: Document;
|
document: Document;
|
||||||
@@ -266,15 +268,7 @@ function DocumentMenu({
|
|||||||
visible: !!can.update && user.separateEditMode,
|
visible: !!can.update && user.separateEditMode,
|
||||||
icon: <EditIcon />,
|
icon: <EditIcon />,
|
||||||
},
|
},
|
||||||
{
|
actionToMenuItem(createNestedDocument, context),
|
||||||
type: "route",
|
|
||||||
title: t("New nested document"),
|
|
||||||
to: newDocumentPath(document.collectionId, {
|
|
||||||
parentDocumentId: document.id,
|
|
||||||
}),
|
|
||||||
visible: !!can.createChildDocument,
|
|
||||||
icon: <NewDocumentIcon />,
|
|
||||||
},
|
|
||||||
actionToMenuItem(importDocument, context),
|
actionToMenuItem(importDocument, context),
|
||||||
actionToMenuItem(createTemplate, context),
|
actionToMenuItem(createTemplate, context),
|
||||||
actionToMenuItem(duplicateDocument, context),
|
actionToMenuItem(duplicateDocument, context),
|
||||||
@@ -283,6 +277,7 @@ function DocumentMenu({
|
|||||||
actionToMenuItem(archiveDocument, context),
|
actionToMenuItem(archiveDocument, context),
|
||||||
actionToMenuItem(moveDocument, context),
|
actionToMenuItem(moveDocument, context),
|
||||||
actionToMenuItem(pinDocument, context),
|
actionToMenuItem(pinDocument, context),
|
||||||
|
actionToMenuItem(createDocumentFromTemplate, context),
|
||||||
{
|
{
|
||||||
type: "separator",
|
type: "separator",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import useCurrentTeam from "~/hooks/useCurrentTeam";
|
|||||||
import usePolicy from "~/hooks/usePolicy";
|
import usePolicy from "~/hooks/usePolicy";
|
||||||
import useStores from "~/hooks/useStores";
|
import useStores from "~/hooks/useStores";
|
||||||
import { MenuItem } from "~/types";
|
import { MenuItem } from "~/types";
|
||||||
import { newDocumentPath } from "~/utils/routeHelpers";
|
import { newTemplatePath } from "~/utils/routeHelpers";
|
||||||
|
|
||||||
function NewTemplateMenu() {
|
function NewTemplateMenu() {
|
||||||
const menu = useMenuState({
|
const menu = useMenuState({
|
||||||
@@ -24,6 +24,11 @@ function NewTemplateMenu() {
|
|||||||
const team = useCurrentTeam();
|
const team = useCurrentTeam();
|
||||||
const { collections, policies } = useStores();
|
const { collections, policies } = useStores();
|
||||||
const can = usePolicy(team);
|
const can = usePolicy(team);
|
||||||
|
React.useEffect(() => {
|
||||||
|
void collections.fetchPage({
|
||||||
|
limit: 100,
|
||||||
|
});
|
||||||
|
}, [collections]);
|
||||||
|
|
||||||
const items = React.useMemo(
|
const items = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -33,9 +38,7 @@ function NewTemplateMenu() {
|
|||||||
if (can.update) {
|
if (can.update) {
|
||||||
filtered.push({
|
filtered.push({
|
||||||
type: "route",
|
type: "route",
|
||||||
to: newDocumentPath(collection.id, {
|
to: newTemplatePath(collection.id),
|
||||||
template: true,
|
|
||||||
}),
|
|
||||||
title: <CollectionName>{collection.name}</CollectionName>,
|
title: <CollectionName>{collection.name}</CollectionName>,
|
||||||
icon: <CollectionIcon collection={collection} />,
|
icon: <CollectionIcon collection={collection} />,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import Button from "~/components/Button";
|
|||||||
import ContextMenu from "~/components/ContextMenu";
|
import ContextMenu from "~/components/ContextMenu";
|
||||||
import MenuItem from "~/components/ContextMenu/MenuItem";
|
import MenuItem from "~/components/ContextMenu/MenuItem";
|
||||||
import Separator from "~/components/ContextMenu/Separator";
|
import Separator from "~/components/ContextMenu/Separator";
|
||||||
|
import EmojiIcon from "~/components/Icons/EmojiIcon";
|
||||||
import useCurrentUser from "~/hooks/useCurrentUser";
|
import useCurrentUser from "~/hooks/useCurrentUser";
|
||||||
import useStores from "~/hooks/useStores";
|
import useStores from "~/hooks/useStores";
|
||||||
import { replaceTitleVariables } from "~/utils/date";
|
import { replaceTitleVariables } from "~/utils/date";
|
||||||
@@ -43,7 +44,9 @@ function TemplatesMenu({ onSelectTemplate, document }: Props) {
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
key={template.id}
|
key={template.id}
|
||||||
onClick={() => onSelectTemplate(template)}
|
onClick={() => onSelectTemplate(template)}
|
||||||
icon={<DocumentIcon />}
|
icon={
|
||||||
|
template.emoji ? <EmojiIcon emoji={template.emoji} /> : <DocumentIcon />
|
||||||
|
}
|
||||||
{...menu}
|
{...menu}
|
||||||
>
|
>
|
||||||
<TemplateItem>
|
<TemplateItem>
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { addDays, differenceInDays } from "date-fns";
|
import { addDays, differenceInDays } from "date-fns";
|
||||||
|
import { t } from "i18next";
|
||||||
import floor from "lodash/floor";
|
import floor from "lodash/floor";
|
||||||
import { action, autorun, computed, observable, set } from "mobx";
|
import { action, autorun, computed, observable, set } from "mobx";
|
||||||
import { ExportContentType } from "@shared/types";
|
import { ExportContentType } from "@shared/types";
|
||||||
import type { NavigationNode } from "@shared/types";
|
import type { NavigationNode } from "@shared/types";
|
||||||
import Storage from "@shared/utils/Storage";
|
import Storage from "@shared/utils/Storage";
|
||||||
import { isRTL } from "@shared/utils/rtl";
|
import { isRTL } from "@shared/utils/rtl";
|
||||||
|
import slugify from "@shared/utils/slugify";
|
||||||
import DocumentsStore from "~/stores/DocumentsStore";
|
import DocumentsStore from "~/stores/DocumentsStore";
|
||||||
import User from "~/models/User";
|
import User from "~/models/User";
|
||||||
import { client } from "~/utils/ApiClient";
|
import { client } from "~/utils/ApiClient";
|
||||||
|
import { settingsPath } from "~/utils/routeHelpers";
|
||||||
import View from "./View";
|
import View from "./View";
|
||||||
import ParanoidModel from "./base/ParanoidModel";
|
import ParanoidModel from "./base/ParanoidModel";
|
||||||
import Field from "./decorators/Field";
|
import Field from "./decorators/Field";
|
||||||
@@ -122,6 +125,9 @@ export default class Document extends ParanoidModel {
|
|||||||
@observable
|
@observable
|
||||||
archivedAt: string;
|
archivedAt: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use path instead
|
||||||
|
*/
|
||||||
@observable
|
@observable
|
||||||
url: string;
|
url: string;
|
||||||
|
|
||||||
@@ -153,9 +159,21 @@ export default class Document extends ParanoidModel {
|
|||||||
return isRTL(this.title);
|
return isRTL(this.title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get path(): string {
|
||||||
|
const prefix = this.template ? settingsPath("templates") : "/doc";
|
||||||
|
|
||||||
|
if (!this.title) {
|
||||||
|
return `${prefix}/untitled-${this.urlId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slugifiedTitle = slugify(this.title);
|
||||||
|
return `${prefix}/${slugifiedTitle}-${this.urlId}`;
|
||||||
|
}
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
get noun(): string {
|
get noun(): string {
|
||||||
return this.template ? "template" : "document";
|
return this.template ? t("template") : t("document");
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
|
|||||||
@@ -10,25 +10,34 @@ import Route from "~/components/ProfiledRoute";
|
|||||||
import WebsocketProvider from "~/components/WebsocketProvider";
|
import WebsocketProvider from "~/components/WebsocketProvider";
|
||||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||||
import usePolicy from "~/hooks/usePolicy";
|
import usePolicy from "~/hooks/usePolicy";
|
||||||
import lazyWithRetry from "~/utils/lazyWithRetry";
|
import lazy from "~/utils/lazyWithRetry";
|
||||||
import { matchDocumentSlug as slug } from "~/utils/routeHelpers";
|
import {
|
||||||
|
archivePath,
|
||||||
|
draftsPath,
|
||||||
|
homePath,
|
||||||
|
searchPath,
|
||||||
|
settingsPath,
|
||||||
|
matchDocumentSlug as slug,
|
||||||
|
trashPath,
|
||||||
|
} from "~/utils/routeHelpers";
|
||||||
|
|
||||||
const SettingsRoutes = lazyWithRetry(() => import("./settings"));
|
const SettingsRoutes = lazy(() => import("./settings"));
|
||||||
const Archive = lazyWithRetry(() => import("~/scenes/Archive"));
|
const Archive = lazy(() => import("~/scenes/Archive"));
|
||||||
const Collection = lazyWithRetry(() => import("~/scenes/Collection"));
|
const Collection = lazy(() => import("~/scenes/Collection"));
|
||||||
const Document = lazyWithRetry(() => import("~/scenes/Document"));
|
const Document = lazy(() => import("~/scenes/Document"));
|
||||||
const Drafts = lazyWithRetry(() => import("~/scenes/Drafts"));
|
const Drafts = lazy(() => import("~/scenes/Drafts"));
|
||||||
const Home = lazyWithRetry(() => import("~/scenes/Home"));
|
const Home = lazy(() => import("~/scenes/Home"));
|
||||||
const Templates = lazyWithRetry(() => import("~/scenes/Templates"));
|
const Search = lazy(() => import("~/scenes/Search"));
|
||||||
const Search = lazyWithRetry(() => import("~/scenes/Search"));
|
const Trash = lazy(() => import("~/scenes/Trash"));
|
||||||
const Trash = lazyWithRetry(() => import("~/scenes/Trash"));
|
|
||||||
|
|
||||||
const RedirectDocument = ({
|
const RedirectDocument = ({
|
||||||
match,
|
match,
|
||||||
}: RouteComponentProps<{ documentSlug: string }>) => (
|
}: RouteComponentProps<{ documentSlug: string }>) => (
|
||||||
<Redirect
|
<Redirect
|
||||||
to={
|
to={
|
||||||
match.params.documentSlug ? `/doc/${match.params.documentSlug}` : "/home"
|
match.params.documentSlug
|
||||||
|
? `/doc/${match.params.documentSlug}`
|
||||||
|
: homePath()
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -49,24 +58,18 @@ function AuthenticatedRoutes() {
|
|||||||
>
|
>
|
||||||
<Switch>
|
<Switch>
|
||||||
{can.createDocument && (
|
{can.createDocument && (
|
||||||
<Route exact path="/templates" component={Templates} />
|
<Route exact path={draftsPath()} component={Drafts} />
|
||||||
)}
|
)}
|
||||||
{can.createDocument && (
|
{can.createDocument && (
|
||||||
<Route exact path="/templates/:sort" component={Templates} />
|
<Route exact path={archivePath()} component={Archive} />
|
||||||
)}
|
)}
|
||||||
{can.createDocument && (
|
{can.createDocument && (
|
||||||
<Route exact path="/drafts" component={Drafts} />
|
<Route exact path={trashPath()} component={Trash} />
|
||||||
)}
|
)}
|
||||||
{can.createDocument && (
|
<Route path={`${homePath()}/:tab?`} component={Home} />
|
||||||
<Route exact path="/archive" component={Archive} />
|
<Redirect from="/dashboard" to={homePath()} />
|
||||||
)}
|
<Redirect exact from="/starred" to={homePath()} />
|
||||||
{can.createDocument && (
|
<Redirect exact from="/templates" to={settingsPath("templates")} />
|
||||||
<Route exact path="/trash" component={Trash} />
|
|
||||||
)}
|
|
||||||
<Redirect from="/dashboard" to="/home" />
|
|
||||||
<Route path="/home/:tab" component={Home} />
|
|
||||||
<Route path="/home" component={Home} />
|
|
||||||
<Redirect exact from="/starred" to="/home" />
|
|
||||||
<Redirect exact from="/collections/*" to="/collection/*" />
|
<Redirect exact from="/collections/*" to="/collection/*" />
|
||||||
<Route exact path="/collection/:id/new" component={DocumentNew} />
|
<Route exact path="/collection/:id/new" component={DocumentNew} />
|
||||||
<Route exact path="/collection/:id/:tab" component={Collection} />
|
<Route exact path="/collection/:id/:tab" component={Collection} />
|
||||||
@@ -81,8 +84,7 @@ function AuthenticatedRoutes() {
|
|||||||
<Route exact path={`/doc/${slug}/insights`} component={Document} />
|
<Route exact path={`/doc/${slug}/insights`} component={Document} />
|
||||||
<Route exact path={`/doc/${slug}/edit`} component={Document} />
|
<Route exact path={`/doc/${slug}/edit`} component={Document} />
|
||||||
<Route path={`/doc/${slug}`} component={Document} />
|
<Route path={`/doc/${slug}`} component={Document} />
|
||||||
<Route exact path="/search" component={Search} />
|
<Route exact path={`${searchPath()}/:term?`} component={Search} />
|
||||||
<Route exact path="/search/:term" component={Search} />
|
|
||||||
<Route path="/404" component={Error404} />
|
<Route path="/404" component={Error404} />
|
||||||
<SettingsRoutes />
|
<SettingsRoutes />
|
||||||
<Route component={Error404} />
|
<Route component={Error404} />
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Switch } from "react-router-dom";
|
import { RouteComponentProps, Switch } from "react-router-dom";
|
||||||
|
import DocumentNew from "~/scenes/DocumentNew";
|
||||||
import Error404 from "~/scenes/Error404";
|
import Error404 from "~/scenes/Error404";
|
||||||
import Route from "~/components/ProfiledRoute";
|
import Route from "~/components/ProfiledRoute";
|
||||||
import useSettingsConfig from "~/hooks/useSettingsConfig";
|
import useSettingsConfig from "~/hooks/useSettingsConfig";
|
||||||
|
import lazy from "~/utils/lazyWithRetry";
|
||||||
|
import { matchDocumentSlug, settingsPath } from "~/utils/routeHelpers";
|
||||||
|
|
||||||
|
const Document = lazy(() => import("~/scenes/Document"));
|
||||||
|
|
||||||
export default function SettingsRoutes() {
|
export default function SettingsRoutes() {
|
||||||
const configs = useSettingsConfig();
|
const configs = useSettingsConfig();
|
||||||
@@ -17,6 +22,18 @@ export default function SettingsRoutes() {
|
|||||||
component={config.component}
|
component={config.component}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path={`${settingsPath("templates")}/${matchDocumentSlug}`}
|
||||||
|
component={Document}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path={`${settingsPath("templates")}/new`}
|
||||||
|
component={(props: RouteComponentProps) => (
|
||||||
|
<DocumentNew {...props} template />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Route component={Error404} />
|
<Route component={Error404} />
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const EmojiPicker = React.lazy(() => import("~/components/EmojiPicker"));
|
|||||||
type Props = {
|
type Props = {
|
||||||
/** ID of the associated document */
|
/** ID of the associated document */
|
||||||
documentId: string;
|
documentId: string;
|
||||||
/** Document to display */
|
/** Title to display */
|
||||||
title: string;
|
title: string;
|
||||||
/** Emoji to display */
|
/** Emoji to display */
|
||||||
emoji?: string | null;
|
emoji?: string | null;
|
||||||
@@ -247,7 +247,7 @@ const DocumentTitle = React.forwardRef(function _DocumentTitle(
|
|||||||
value={title}
|
value={title}
|
||||||
$emojiPickerIsOpen={emojiPickerIsOpen}
|
$emojiPickerIsOpen={emojiPickerIsOpen}
|
||||||
$containsEmoji={!!emoji}
|
$containsEmoji={!!emoji}
|
||||||
autoFocus={!document.title}
|
autoFocus={!title}
|
||||||
maxLength={DocumentValidation.maxTitleLength}
|
maxLength={DocumentValidation.maxTitleLength}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
dir="auto"
|
dir="auto"
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import EmojiIcon from "~/components/Icons/EmojiIcon";
|
|||||||
import Star from "~/components/Star";
|
import Star from "~/components/Star";
|
||||||
import Tooltip from "~/components/Tooltip";
|
import Tooltip from "~/components/Tooltip";
|
||||||
import { publishDocument } from "~/actions/definitions/documents";
|
import { publishDocument } from "~/actions/definitions/documents";
|
||||||
|
import { navigateToTemplateSettings } from "~/actions/definitions/navigation";
|
||||||
import { restoreRevision } from "~/actions/definitions/revisions";
|
import { restoreRevision } from "~/actions/definitions/revisions";
|
||||||
import useActionContext from "~/hooks/useActionContext";
|
import useActionContext from "~/hooks/useActionContext";
|
||||||
import useMobile from "~/hooks/useMobile";
|
import useMobile from "~/hooks/useMobile";
|
||||||
@@ -36,7 +37,7 @@ import NewChildDocumentMenu from "~/menus/NewChildDocumentMenu";
|
|||||||
import TableOfContentsMenu from "~/menus/TableOfContentsMenu";
|
import TableOfContentsMenu from "~/menus/TableOfContentsMenu";
|
||||||
import TemplatesMenu from "~/menus/TemplatesMenu";
|
import TemplatesMenu from "~/menus/TemplatesMenu";
|
||||||
import { metaDisplay } from "~/utils/keyboard";
|
import { metaDisplay } from "~/utils/keyboard";
|
||||||
import { newDocumentPath, documentEditPath } from "~/utils/routeHelpers";
|
import { documentEditPath } from "~/utils/routeHelpers";
|
||||||
import ObservingBanner from "./ObservingBanner";
|
import ObservingBanner from "./ObservingBanner";
|
||||||
import PublicBreadcrumb from "./PublicBreadcrumb";
|
import PublicBreadcrumb from "./PublicBreadcrumb";
|
||||||
import ShareButton from "./ShareButton";
|
import ShareButton from "./ShareButton";
|
||||||
@@ -243,31 +244,43 @@ function DocumentHeader({
|
|||||||
{!isEditing &&
|
{!isEditing &&
|
||||||
!isDeleted &&
|
!isDeleted &&
|
||||||
!isRevision &&
|
!isRevision &&
|
||||||
(!isMobile || !isTemplate) &&
|
!isTemplate &&
|
||||||
|
!isMobile &&
|
||||||
document.collectionId && (
|
document.collectionId && (
|
||||||
<Action>
|
<Action>
|
||||||
<ShareButton document={document} />
|
<ShareButton document={document} />
|
||||||
</Action>
|
</Action>
|
||||||
)}
|
)}
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
<>
|
<Action>
|
||||||
<Action>
|
<Tooltip
|
||||||
<Tooltip
|
tooltip={t("Save")}
|
||||||
tooltip={t("Save")}
|
shortcut={`${metaDisplay}+enter`}
|
||||||
shortcut={`${metaDisplay}+enter`}
|
delay={500}
|
||||||
delay={500}
|
placement="bottom"
|
||||||
placement="bottom"
|
>
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={savingIsDisabled}
|
||||||
|
neutral={isDraft}
|
||||||
>
|
>
|
||||||
<Button
|
{isDraft ? t("Save draft") : t("Done editing")}
|
||||||
onClick={handleSave}
|
</Button>
|
||||||
disabled={savingIsDisabled}
|
</Tooltip>
|
||||||
neutral={isDraft}
|
</Action>
|
||||||
>
|
)}
|
||||||
{isDraft ? t("Save draft") : t("Done editing")}
|
{isTemplate && (
|
||||||
</Button>
|
<Action>
|
||||||
</Tooltip>
|
<Button
|
||||||
</Action>
|
context={context}
|
||||||
</>
|
action={navigateToTemplateSettings}
|
||||||
|
disabled={savingIsDisabled}
|
||||||
|
neutral={isDraft}
|
||||||
|
hideIcon
|
||||||
|
>
|
||||||
|
{t("Done editing")}
|
||||||
|
</Button>
|
||||||
|
</Action>
|
||||||
)}
|
)}
|
||||||
{can.update &&
|
{can.update &&
|
||||||
!isEditing &&
|
!isEditing &&
|
||||||
@@ -296,23 +309,6 @@ function DocumentHeader({
|
|||||||
/>
|
/>
|
||||||
</Action>
|
</Action>
|
||||||
)}
|
)}
|
||||||
{can.update &&
|
|
||||||
!isEditing &&
|
|
||||||
isTemplate &&
|
|
||||||
!isDraft &&
|
|
||||||
!isRevision && (
|
|
||||||
<Action>
|
|
||||||
<Button
|
|
||||||
icon={<PlusIcon />}
|
|
||||||
as={Link}
|
|
||||||
to={newDocumentPath(document.collectionId, {
|
|
||||||
templateId: document.id,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{t("New from template")}
|
|
||||||
</Button>
|
|
||||||
</Action>
|
|
||||||
)}
|
|
||||||
{revision && revision.createdAt !== document.updatedAt && (
|
{revision && revision.createdAt !== document.updatedAt && (
|
||||||
<Action>
|
<Action>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import queryString from "query-string";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -7,23 +6,31 @@ import { useHistory, useLocation, useRouteMatch } from "react-router-dom";
|
|||||||
import CenteredContent from "~/components/CenteredContent";
|
import CenteredContent from "~/components/CenteredContent";
|
||||||
import Flex from "~/components/Flex";
|
import Flex from "~/components/Flex";
|
||||||
import PlaceholderDocument from "~/components/PlaceholderDocument";
|
import PlaceholderDocument from "~/components/PlaceholderDocument";
|
||||||
|
import useCurrentUser from "~/hooks/useCurrentUser";
|
||||||
|
import useQuery from "~/hooks/useQuery";
|
||||||
import useStores from "~/hooks/useStores";
|
import useStores from "~/hooks/useStores";
|
||||||
import useToasts from "~/hooks/useToasts";
|
import useToasts from "~/hooks/useToasts";
|
||||||
import { documentEditPath } from "~/utils/routeHelpers";
|
import { documentEditPath, documentPath } from "~/utils/routeHelpers";
|
||||||
|
|
||||||
function DocumentNew() {
|
type Props = {
|
||||||
|
// If true, the document will be created as a template.
|
||||||
|
template?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function DocumentNew({ template }: Props) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const query = useQuery();
|
||||||
|
const user = useCurrentUser();
|
||||||
const match = useRouteMatch<{ id?: string }>();
|
const match = useRouteMatch<{ id?: string }>();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { documents, collections } = useStores();
|
const { documents, collections } = useStores();
|
||||||
const { showToast } = useToasts();
|
const { showToast } = useToasts();
|
||||||
const id = match.params.id || "";
|
const id = match.params.id || query.get("collectionId");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function createDocument() {
|
async function createDocument() {
|
||||||
const params = queryString.parse(location.search);
|
const parentDocumentId = query.get("parentDocumentId") ?? undefined;
|
||||||
const parentDocumentId = params.parentDocumentId?.toString();
|
|
||||||
const parentDocument = parentDocumentId
|
const parentDocument = parentDocumentId
|
||||||
? documents.get(parentDocumentId)
|
? documents.get(parentDocumentId)
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -37,12 +44,17 @@ function DocumentNew() {
|
|||||||
collectionId: collection?.id,
|
collectionId: collection?.id,
|
||||||
parentDocumentId,
|
parentDocumentId,
|
||||||
fullWidth: parentDocument?.fullWidth,
|
fullWidth: parentDocument?.fullWidth,
|
||||||
templateId: params.templateId?.toString(),
|
templateId: query.get("templateId") ?? undefined,
|
||||||
template: params.template === "true" ? true : false,
|
template,
|
||||||
title: "",
|
title: "",
|
||||||
text: "",
|
text: "",
|
||||||
});
|
});
|
||||||
history.replace(documentEditPath(document), location.state);
|
history.replace(
|
||||||
|
template || !user.separateEditMode
|
||||||
|
? documentPath(document)
|
||||||
|
: documentEditPath(document),
|
||||||
|
location.state
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(t("Couldn’t create the document, try again?"), {
|
showToast(t("Couldn’t create the document, try again?"), {
|
||||||
type: "error",
|
type: "error",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { ShapesIcon } from "outline-icons";
|
import { ShapesIcon } from "outline-icons";
|
||||||
|
import queryString from "query-string";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
|
||||||
import { Action } from "~/components/Actions";
|
import { Action } from "~/components/Actions";
|
||||||
import Empty from "~/components/Empty";
|
import Empty from "~/components/Empty";
|
||||||
import Heading from "~/components/Heading";
|
import Heading from "~/components/Heading";
|
||||||
@@ -10,18 +10,18 @@ import PaginatedDocumentList from "~/components/PaginatedDocumentList";
|
|||||||
import Scene from "~/components/Scene";
|
import Scene from "~/components/Scene";
|
||||||
import Tab from "~/components/Tab";
|
import Tab from "~/components/Tab";
|
||||||
import Tabs from "~/components/Tabs";
|
import Tabs from "~/components/Tabs";
|
||||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
import Text from "~/components/Text";
|
||||||
import usePolicy from "~/hooks/usePolicy";
|
import useQuery from "~/hooks/useQuery";
|
||||||
import useStores from "~/hooks/useStores";
|
import useStores from "~/hooks/useStores";
|
||||||
import NewTemplateMenu from "~/menus/NewTemplateMenu";
|
import NewTemplateMenu from "~/menus/NewTemplateMenu";
|
||||||
|
import { settingsPath } from "~/utils/routeHelpers";
|
||||||
|
|
||||||
function Templates(props: RouteComponentProps<{ sort: string }>) {
|
function Templates() {
|
||||||
const { documents } = useStores();
|
const { documents } = useStores();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const team = useCurrentTeam();
|
const param = useQuery();
|
||||||
const { fetchTemplates, templates, templatesAlphabetical } = documents;
|
const { fetchTemplates, templates, templatesAlphabetical } = documents;
|
||||||
const { sort } = props.match.params;
|
const sort = param.get("sort") || "recent";
|
||||||
const can = usePolicy(team);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Scene
|
<Scene
|
||||||
@@ -34,26 +34,33 @@ function Templates(props: RouteComponentProps<{ sort: string }>) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Heading>{t("Templates")}</Heading>
|
<Heading>{t("Templates")}</Heading>
|
||||||
|
<Text type="secondary">
|
||||||
|
<Trans>
|
||||||
|
You can create templates to help your team create consistent and
|
||||||
|
accurate documentation.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
|
||||||
<PaginatedDocumentList
|
<PaginatedDocumentList
|
||||||
heading={
|
heading={
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab to="/templates" exact>
|
<Tab to={settingsPath("templates")} exactQueryString>
|
||||||
{t("Recently updated")}
|
{t("Recently updated")}
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab to="/templates/alphabetical" exact>
|
<Tab
|
||||||
|
to={{
|
||||||
|
pathname: settingsPath("templates"),
|
||||||
|
search: queryString.stringify({
|
||||||
|
sort: "alphabetical",
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
exactQueryString
|
||||||
|
>
|
||||||
{t("Alphabetical")}
|
{t("Alphabetical")}
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
}
|
}
|
||||||
empty={
|
empty={<Empty>{t("There are no templates just yet.")}</Empty>}
|
||||||
<Empty>
|
|
||||||
{t("There are no templates just yet.")}{" "}
|
|
||||||
{can.createDocument &&
|
|
||||||
t(
|
|
||||||
"You can create templates to help your team create consistent and accurate documentation."
|
|
||||||
)}
|
|
||||||
</Empty>
|
|
||||||
}
|
|
||||||
fetch={fetchTemplates}
|
fetch={fetchTemplates}
|
||||||
documents={sort === "alphabetical" ? templatesAlphabetical : templates}
|
documents={sort === "alphabetical" ? templatesAlphabetical : templates}
|
||||||
showCollection
|
showCollection
|
||||||
@@ -11,10 +11,6 @@ export function draftsPath(): string {
|
|||||||
return "/drafts";
|
return "/drafts";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function templatesPath(): string {
|
|
||||||
return "/templates";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function archivePath(): string {
|
export function archivePath(): string {
|
||||||
return "/archive";
|
return "/archive";
|
||||||
}
|
}
|
||||||
@@ -50,22 +46,22 @@ export function updateCollectionPath(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function documentPath(doc: Document): string {
|
export function documentPath(doc: Document): string {
|
||||||
return doc.url;
|
return doc.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function documentEditPath(doc: Document): string {
|
export function documentEditPath(doc: Document): string {
|
||||||
return `${doc.url}/edit`;
|
return `${documentPath(doc)}/edit`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function documentInsightsPath(doc: Document): string {
|
export function documentInsightsPath(doc: Document): string {
|
||||||
return `${doc.url}/insights`;
|
return `${documentPath(doc)}/insights`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function documentHistoryPath(
|
export function documentHistoryPath(
|
||||||
doc: Document,
|
doc: Document,
|
||||||
revisionId?: string
|
revisionId?: string
|
||||||
): string {
|
): string {
|
||||||
let base = `${doc.url}/history`;
|
let base = `${documentPath(doc)}/history`;
|
||||||
if (revisionId) {
|
if (revisionId) {
|
||||||
base += `/${revisionId}`;
|
base += `/${revisionId}`;
|
||||||
}
|
}
|
||||||
@@ -84,12 +80,15 @@ export function updateDocumentPath(oldUrl: string, document: Document): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function newTemplatePath(collectionId: string) {
|
||||||
|
return settingsPath("templates") + `/new?collectionId=${collectionId}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function newDocumentPath(
|
export function newDocumentPath(
|
||||||
collectionId?: string | null,
|
collectionId?: string | null,
|
||||||
params: {
|
params: {
|
||||||
parentDocumentId?: string;
|
parentDocumentId?: string;
|
||||||
templateId?: string;
|
templateId?: string;
|
||||||
template?: boolean;
|
|
||||||
} = {}
|
} = {}
|
||||||
): string {
|
): string {
|
||||||
return collectionId
|
return collectionId
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ type Props = {
|
|||||||
id?: string;
|
id?: string;
|
||||||
urlId?: string;
|
urlId?: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
emoji?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
state?: Buffer;
|
state?: Buffer;
|
||||||
publish?: boolean;
|
publish?: boolean;
|
||||||
@@ -28,6 +29,7 @@ type Props = {
|
|||||||
export default async function documentCreator({
|
export default async function documentCreator({
|
||||||
title = "",
|
title = "",
|
||||||
text = "",
|
text = "",
|
||||||
|
emoji,
|
||||||
state,
|
state,
|
||||||
id,
|
id,
|
||||||
urlId,
|
urlId,
|
||||||
@@ -81,6 +83,7 @@ export default async function documentCreator({
|
|||||||
fullWidth,
|
fullWidth,
|
||||||
publishedAt,
|
publishedAt,
|
||||||
importId,
|
importId,
|
||||||
|
emoji: templateDocument ? templateDocument.emoji : emoji,
|
||||||
title: templateDocument
|
title: templateDocument
|
||||||
? DocumentHelper.replaceTemplateVariables(templateDocument.title, user)
|
? DocumentHelper.replaceTemplateVariables(templateDocument.title, user)
|
||||||
: title,
|
: title,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import randomstring from "randomstring";
|
import randomstring from "randomstring";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import slugify from "@shared/utils/slugify";
|
||||||
import {
|
import {
|
||||||
buildUser,
|
buildUser,
|
||||||
buildGroup,
|
buildGroup,
|
||||||
@@ -7,7 +8,6 @@ import {
|
|||||||
buildTeam,
|
buildTeam,
|
||||||
buildDocument,
|
buildDocument,
|
||||||
} from "@server/test/factories";
|
} from "@server/test/factories";
|
||||||
import slugify from "@server/utils/slugify";
|
|
||||||
import Collection from "./Collection";
|
import Collection from "./Collection";
|
||||||
import Document from "./Document";
|
import Document from "./Document";
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ import isUUID from "validator/lib/isUUID";
|
|||||||
import type { CollectionSort } from "@shared/types";
|
import type { CollectionSort } from "@shared/types";
|
||||||
import { CollectionPermission, NavigationNode } from "@shared/types";
|
import { CollectionPermission, NavigationNode } from "@shared/types";
|
||||||
import { sortNavigationNodes } from "@shared/utils/collections";
|
import { sortNavigationNodes } from "@shared/utils/collections";
|
||||||
|
import slugify from "@shared/utils/slugify";
|
||||||
import { SLUG_URL_REGEX } from "@shared/utils/urlHelpers";
|
import { SLUG_URL_REGEX } from "@shared/utils/urlHelpers";
|
||||||
import { CollectionValidation } from "@shared/validations";
|
import { CollectionValidation } from "@shared/validations";
|
||||||
import slugify from "@server/utils/slugify";
|
|
||||||
import CollectionGroup from "./CollectionGroup";
|
import CollectionGroup from "./CollectionGroup";
|
||||||
import CollectionUser from "./CollectionUser";
|
import CollectionUser from "./CollectionUser";
|
||||||
import Document from "./Document";
|
import Document from "./Document";
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import slugify from "@shared/utils/slugify";
|
||||||
import Document from "@server/models/Document";
|
import Document from "@server/models/Document";
|
||||||
import {
|
import {
|
||||||
buildDocument,
|
buildDocument,
|
||||||
@@ -6,7 +7,6 @@ import {
|
|||||||
buildTeam,
|
buildTeam,
|
||||||
buildUser,
|
buildUser,
|
||||||
} from "@server/test/factories";
|
} from "@server/test/factories";
|
||||||
import slugify from "@server/utils/slugify";
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ import {
|
|||||||
import isUUID from "validator/lib/isUUID";
|
import isUUID from "validator/lib/isUUID";
|
||||||
import type { NavigationNode } from "@shared/types";
|
import type { NavigationNode } from "@shared/types";
|
||||||
import getTasks from "@shared/utils/getTasks";
|
import getTasks from "@shared/utils/getTasks";
|
||||||
|
import slugify from "@shared/utils/slugify";
|
||||||
import { SLUG_URL_REGEX } from "@shared/utils/urlHelpers";
|
import { SLUG_URL_REGEX } from "@shared/utils/urlHelpers";
|
||||||
import { DocumentValidation } from "@shared/validations";
|
import { DocumentValidation } from "@shared/validations";
|
||||||
import slugify from "@server/utils/slugify";
|
|
||||||
import Backlink from "./Backlink";
|
import Backlink from "./Backlink";
|
||||||
import Collection from "./Collection";
|
import Collection from "./Collection";
|
||||||
import FileOperation from "./FileOperation";
|
import FileOperation from "./FileOperation";
|
||||||
|
|||||||
@@ -179,11 +179,27 @@ allow(User, "move", Document, (user, document) => {
|
|||||||
return user.teamId === document.teamId;
|
return user.teamId === document.teamId;
|
||||||
});
|
});
|
||||||
|
|
||||||
allow(User, ["pin", "unpin"], Document, (user, document) => {
|
allow(User, "pin", Document, (user, document) => {
|
||||||
if (!document || document.isDraft) {
|
if (
|
||||||
|
!document ||
|
||||||
|
document.isDraft ||
|
||||||
|
!document.isActive ||
|
||||||
|
document.template
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (document.template) {
|
invariant(
|
||||||
|
document.collection,
|
||||||
|
"collection is missing, did you forget to include in the query scope?"
|
||||||
|
);
|
||||||
|
if (cannot(user, "update", document.collection)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return user.teamId === document.teamId;
|
||||||
|
});
|
||||||
|
|
||||||
|
allow(User, "unpin", Document, (user, document) => {
|
||||||
|
if (!document || document.isDraft || document.template) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
invariant(
|
invariant(
|
||||||
@@ -197,10 +213,12 @@ allow(User, ["pin", "unpin"], Document, (user, document) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
allow(User, ["subscribe", "unsubscribe"], Document, (user, document) => {
|
allow(User, ["subscribe", "unsubscribe"], Document, (user, document) => {
|
||||||
if (!document || !document.isActive || document.isDraft) {
|
if (
|
||||||
return false;
|
!document ||
|
||||||
}
|
!document.isActive ||
|
||||||
if (document.template) {
|
document.isDraft ||
|
||||||
|
document.template
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
invariant(
|
invariant(
|
||||||
@@ -284,7 +302,12 @@ allow(User, "restore", Document, (user, document) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
allow(User, "archive", Document, (user, document) => {
|
allow(User, "archive", Document, (user, document) => {
|
||||||
if (!document || !document.isActive || document.isDraft) {
|
if (
|
||||||
|
!document ||
|
||||||
|
!document.isActive ||
|
||||||
|
document.isDraft ||
|
||||||
|
document.template
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
invariant(
|
invariant(
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Op, ScopeOptions, WhereOptions } from "sequelize";
|
|||||||
import { TeamPreference } from "@shared/types";
|
import { TeamPreference } from "@shared/types";
|
||||||
import { subtractDate } from "@shared/utils/date";
|
import { subtractDate } from "@shared/utils/date";
|
||||||
import { bytesToHumanReadable } from "@shared/utils/files";
|
import { bytesToHumanReadable } from "@shared/utils/files";
|
||||||
|
import slugify from "@shared/utils/slugify";
|
||||||
import documentCreator from "@server/commands/documentCreator";
|
import documentCreator from "@server/commands/documentCreator";
|
||||||
import documentImporter from "@server/commands/documentImporter";
|
import documentImporter from "@server/commands/documentImporter";
|
||||||
import documentLoader from "@server/commands/documentLoader";
|
import documentLoader from "@server/commands/documentLoader";
|
||||||
@@ -55,7 +56,6 @@ import ZipHelper from "@server/utils/ZipHelper";
|
|||||||
import { getFileFromRequest } from "@server/utils/koa";
|
import { getFileFromRequest } from "@server/utils/koa";
|
||||||
import parseAttachmentIds from "@server/utils/parseAttachmentIds";
|
import parseAttachmentIds from "@server/utils/parseAttachmentIds";
|
||||||
import { getTeamFromContext } from "@server/utils/passport";
|
import { getTeamFromContext } from "@server/utils/passport";
|
||||||
import slugify from "@server/utils/slugify";
|
|
||||||
import { assertPresent } from "@server/validation";
|
import { assertPresent } from "@server/validation";
|
||||||
import pagination from "../middlewares/pagination";
|
import pagination from "../middlewares/pagination";
|
||||||
import * as T from "./schema";
|
import * as T from "./schema";
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Router from "koa-router";
|
import Router from "koa-router";
|
||||||
import { Op } from "sequelize";
|
import { Op } from "sequelize";
|
||||||
import { RevisionHelper } from "@shared/utils/RevisionHelper";
|
import { RevisionHelper } from "@shared/utils/RevisionHelper";
|
||||||
|
import slugify from "@shared/utils/slugify";
|
||||||
import { ValidationError } from "@server/errors";
|
import { ValidationError } from "@server/errors";
|
||||||
import auth from "@server/middlewares/authentication";
|
import auth from "@server/middlewares/authentication";
|
||||||
import validate from "@server/middlewares/validate";
|
import validate from "@server/middlewares/validate";
|
||||||
@@ -9,7 +10,6 @@ import DocumentHelper from "@server/models/helpers/DocumentHelper";
|
|||||||
import { authorize } from "@server/policies";
|
import { authorize } from "@server/policies";
|
||||||
import { presentRevision } from "@server/presenters";
|
import { presentRevision } from "@server/presenters";
|
||||||
import { APIContext } from "@server/types";
|
import { APIContext } from "@server/types";
|
||||||
import slugify from "@server/utils/slugify";
|
|
||||||
import pagination from "../middlewares/pagination";
|
import pagination from "../middlewares/pagination";
|
||||||
import * as T from "./schema";
|
import * as T from "./schema";
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import slug from "slug";
|
|
||||||
|
|
||||||
slug.defaults.mode = "rfc3986";
|
|
||||||
|
|
||||||
export default function slugify(text: string): string {
|
|
||||||
return slug(text, {
|
|
||||||
remove: /[.]/g,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -18,11 +18,13 @@
|
|||||||
"Development": "Development",
|
"Development": "Development",
|
||||||
"Open document": "Open document",
|
"Open document": "Open document",
|
||||||
"New document": "New document",
|
"New document": "New document",
|
||||||
|
"New from template": "New from template",
|
||||||
|
"New nested document": "New nested document",
|
||||||
"Publish": "Publish",
|
"Publish": "Publish",
|
||||||
"Document published": "Document published",
|
"Published {{ documentName }}": "Published {{ documentName }}",
|
||||||
"Publish document": "Publish document",
|
"Publish document": "Publish document",
|
||||||
"Unpublish": "Unpublish",
|
"Unpublish": "Unpublish",
|
||||||
"Document unpublished": "Document unpublished",
|
"Unpublished {{ documentName }}": "Unpublished {{ documentName }}",
|
||||||
"Subscribe": "Subscribe",
|
"Subscribe": "Subscribe",
|
||||||
"Subscribed to document notifications": "Subscribed to document notifications",
|
"Subscribed to document notifications": "Subscribed to document notifications",
|
||||||
"Unsubscribe": "Unsubscribe",
|
"Unsubscribe": "Unsubscribe",
|
||||||
@@ -61,10 +63,10 @@
|
|||||||
"Insights": "Insights",
|
"Insights": "Insights",
|
||||||
"Home": "Home",
|
"Home": "Home",
|
||||||
"Drafts": "Drafts",
|
"Drafts": "Drafts",
|
||||||
"Templates": "Templates",
|
|
||||||
"Trash": "Trash",
|
"Trash": "Trash",
|
||||||
"Settings": "Settings",
|
"Settings": "Settings",
|
||||||
"Profile": "Profile",
|
"Profile": "Profile",
|
||||||
|
"Templates": "Templates",
|
||||||
"Notifications": "Notifications",
|
"Notifications": "Notifications",
|
||||||
"Preferences": "Preferences",
|
"Preferences": "Preferences",
|
||||||
"API documentation": "API documentation",
|
"API documentation": "API documentation",
|
||||||
@@ -139,7 +141,6 @@
|
|||||||
"Only visible to you": "Only visible to you",
|
"Only visible to you": "Only visible to you",
|
||||||
"Draft": "Draft",
|
"Draft": "Draft",
|
||||||
"Template": "Template",
|
"Template": "Template",
|
||||||
"New doc": "New doc",
|
|
||||||
"You updated": "You updated",
|
"You updated": "You updated",
|
||||||
"{{ userName }} updated": "{{ userName }} updated",
|
"{{ userName }} updated": "{{ userName }} updated",
|
||||||
"You deleted": "You deleted",
|
"You deleted": "You deleted",
|
||||||
@@ -230,9 +231,9 @@
|
|||||||
"No results for {{query}}": "No results for {{query}}",
|
"No results for {{query}}": "No results for {{query}}",
|
||||||
"Logo": "Logo",
|
"Logo": "Logo",
|
||||||
"Move document": "Move document",
|
"Move document": "Move document",
|
||||||
|
"New doc": "New doc",
|
||||||
"You can't reorder documents in an alphabetically sorted collection": "You can't reorder documents in an alphabetically sorted collection",
|
"You can't reorder documents in an alphabetically sorted collection": "You can't reorder documents in an alphabetically sorted collection",
|
||||||
"Collections": "Collections",
|
"Collections": "Collections",
|
||||||
"New nested document": "New nested document",
|
|
||||||
"Document not supported – try Markdown, Plain text, HTML, or Word": "Document not supported – try Markdown, Plain text, HTML, or Word",
|
"Document not supported – try Markdown, Plain text, HTML, or Word": "Document not supported – try Markdown, Plain text, HTML, or Word",
|
||||||
"Empty": "Empty",
|
"Empty": "Empty",
|
||||||
"Go back": "Go back",
|
"Go back": "Go back",
|
||||||
@@ -403,6 +404,8 @@
|
|||||||
"Resend invite": "Resend invite",
|
"Resend invite": "Resend invite",
|
||||||
"Revoke invite": "Revoke invite",
|
"Revoke invite": "Revoke invite",
|
||||||
"Activate account": "Activate account",
|
"Activate account": "Activate account",
|
||||||
|
"template": "template",
|
||||||
|
"document": "document",
|
||||||
"published": "published",
|
"published": "published",
|
||||||
"edited": "edited",
|
"edited": "edited",
|
||||||
"created the collection": "created the collection",
|
"created the collection": "created the collection",
|
||||||
@@ -508,7 +511,6 @@
|
|||||||
"Archived": "Archived",
|
"Archived": "Archived",
|
||||||
"Save draft": "Save draft",
|
"Save draft": "Save draft",
|
||||||
"Done editing": "Done editing",
|
"Done editing": "Done editing",
|
||||||
"New from template": "New from template",
|
|
||||||
"Restore version": "Restore version",
|
"Restore version": "Restore version",
|
||||||
"No history yet": "No history yet",
|
"No history yet": "No history yet",
|
||||||
"Stats": "Stats",
|
"Stats": "Stats",
|
||||||
@@ -580,6 +582,7 @@
|
|||||||
"Document permanently deleted": "Document permanently deleted",
|
"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.",
|
"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.",
|
||||||
"Select a location to publish": "Select a location to publish",
|
"Select a location to publish": "Select a location to publish",
|
||||||
|
"Document published": "Document published",
|
||||||
"Couldn’t publish the document, try again?": "Couldn’t publish the document, try again?",
|
"Couldn’t publish the document, try again?": "Couldn’t publish the document, try again?",
|
||||||
"Publish in <em>{{ location }}</em>": "Publish in <em>{{ location }}</em>",
|
"Publish in <em>{{ location }}</em>": "Publish in <em>{{ location }}</em>",
|
||||||
"view and edit access": "view and edit access",
|
"view and edit access": "view and edit access",
|
||||||
@@ -873,6 +876,9 @@
|
|||||||
"Sharing is currently disabled.": "Sharing is currently disabled.",
|
"Sharing is currently disabled.": "Sharing is currently disabled.",
|
||||||
"You can globally enable and disable public document sharing in the <em>security settings</em>.": "You can globally enable and disable public document sharing in the <em>security settings</em>.",
|
"You can globally enable and disable public document sharing in the <em>security settings</em>.": "You can globally enable and disable public document sharing in the <em>security settings</em>.",
|
||||||
"Documents that have been shared are listed below. Anyone that has the public link can access a read-only version of the document until the link has been revoked.": "Documents that have been shared are listed below. Anyone that has the public link can access a read-only version of the document until the link has been revoked.",
|
"Documents that have been shared are listed below. Anyone that has the public link can access a read-only version of the document until the link has been revoked.": "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.",
|
||||||
|
"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.",
|
||||||
|
"Alphabetical": "Alphabetical",
|
||||||
|
"There are no templates just yet.": "There are no templates just yet.",
|
||||||
"Zapier is a platform that allows {{appName}} to easily integrate with thousands of other business tools. Automate your workflows, sync data, and more.": "Zapier is a platform that allows {{appName}} to easily integrate with thousands of other business tools. Automate your workflows, sync data, and more.",
|
"Zapier is a platform that allows {{appName}} to easily integrate with thousands of other business tools. Automate your workflows, sync data, and more.": "Zapier is a platform that allows {{appName}} to easily integrate with thousands of other business tools. Automate your workflows, sync data, and more.",
|
||||||
"A confirmation code has been sent to your email address, please enter the code below to permanantly destroy this workspace.": "A confirmation code has been sent to your email address, please enter the code below to permanantly destroy this workspace.",
|
"A confirmation code has been sent to your email address, please enter the code below to permanantly destroy this workspace.": "A confirmation code has been sent to your email address, please enter the code below to permanantly destroy this workspace.",
|
||||||
"Confirmation code": "Confirmation code",
|
"Confirmation code": "Confirmation code",
|
||||||
@@ -881,9 +887,6 @@
|
|||||||
"Workspace name": "Workspace name",
|
"Workspace name": "Workspace name",
|
||||||
"Your are creating a new workspace using your current account — <em>{{email}}</em>": "Your are creating a new workspace using your current account — <em>{{email}}</em>",
|
"Your are creating a new workspace using your current account — <em>{{email}}</em>": "Your are creating a new workspace using your current account — <em>{{email}}</em>",
|
||||||
"To create a workspace under another email please sign up from the homepage": "To create a workspace under another email please sign up from the homepage",
|
"To create a workspace under another email please sign up from the homepage": "To create a workspace under another email please sign up from the homepage",
|
||||||
"Alphabetical": "Alphabetical",
|
|
||||||
"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.",
|
"Trash is empty at the moment.": "Trash is empty at the moment.",
|
||||||
"A confirmation code has been sent to your email address, please enter the code below to permanantly destroy your account.": "A confirmation code has been sent to your email address, please enter the code below to permanantly destroy your account.",
|
"A confirmation code has been sent to your email address, please enter the code below to permanantly destroy your account.": "A confirmation code has been sent to your email address, please enter the code below to permanantly destroy your 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 {{appName}} 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 {{appName}} 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 {{appName}} 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 {{appName}} and all your API tokens will be revoked.",
|
||||||
|
|||||||
16
shared/utils/slugify.ts
Normal file
16
shared/utils/slugify.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import slug from "slug";
|
||||||
|
|
||||||
|
slug.defaults.mode = "rfc3986";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a string to a slug that can be used in a URL in kebab-case format,
|
||||||
|
* and remove periods.
|
||||||
|
*
|
||||||
|
* @param text The text to convert
|
||||||
|
* @returns The slugified text
|
||||||
|
*/
|
||||||
|
export default function slugify(text: string): string {
|
||||||
|
return slug(text, {
|
||||||
|
remove: /[.]/g,
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user