mirror of
https://github.com/outline/outline.git
synced 2026-02-27 06:48:47 -06:00
Move collection description into dedicated tab (#8326)
* Move collection description into dedicated tab * Dynamic default * refactor
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import debounce from "lodash/debounce";
|
||||
import { observer } from "mobx-react";
|
||||
import { transparentize } from "polished";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
@@ -8,23 +7,14 @@ import styled from "styled-components";
|
||||
import { richExtensions } from "@shared/editor/nodes";
|
||||
import { s } from "@shared/styles";
|
||||
import Collection from "~/models/Collection";
|
||||
import Arrow from "~/components/Arrow";
|
||||
import ButtonLink from "~/components/ButtonLink";
|
||||
import Editor from "~/components/Editor";
|
||||
import LoadingIndicator from "~/components/LoadingIndicator";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
import BlockMenuExtension from "~/editor/extensions/BlockMenu";
|
||||
import EmojiMenuExtension from "~/editor/extensions/EmojiMenu";
|
||||
import HoverPreviewsExtension from "~/editor/extensions/HoverPreviews";
|
||||
import { withUIExtensions } from "~/editor/extensions";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import Text from "./Text";
|
||||
|
||||
const extensions = [
|
||||
...richExtensions,
|
||||
BlockMenuExtension,
|
||||
EmojiMenuExtension,
|
||||
HoverPreviewsExtension,
|
||||
];
|
||||
const extensions = withUIExtensions(richExtensions);
|
||||
|
||||
type Props = {
|
||||
collection: Collection;
|
||||
@@ -33,33 +23,8 @@ type Props = {
|
||||
function CollectionDescription({ collection }: Props) {
|
||||
const { collections } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const [isExpanded, setExpanded] = React.useState(false);
|
||||
const [isEditing, setEditing] = React.useState(false);
|
||||
const [isDirty, setDirty] = React.useState(false);
|
||||
const can = usePolicy(collection);
|
||||
|
||||
const handleStartEditing = React.useCallback(() => {
|
||||
setEditing(true);
|
||||
}, []);
|
||||
|
||||
const handleStopEditing = React.useCallback(() => {
|
||||
setEditing(false);
|
||||
}, []);
|
||||
|
||||
const handleClickDisclosure = React.useCallback(
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (isExpanded && document.activeElement) {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'blur' does not exist on type 'Element'.
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
setExpanded(!isExpanded);
|
||||
},
|
||||
[isExpanded]
|
||||
);
|
||||
|
||||
const handleSave = React.useMemo(
|
||||
() =>
|
||||
debounce(async (getValue) => {
|
||||
@@ -67,7 +32,6 @@ function CollectionDescription({ collection }: Props) {
|
||||
await collection.save({
|
||||
data: getValue(false),
|
||||
});
|
||||
setDirty(false);
|
||||
} catch (err) {
|
||||
toast.error(t("Sorry, an error occurred saving the collection"));
|
||||
throw err;
|
||||
@@ -76,162 +40,44 @@ function CollectionDescription({ collection }: Props) {
|
||||
[collection, t]
|
||||
);
|
||||
|
||||
const handleChange = React.useCallback(
|
||||
async (getValue) => {
|
||||
setDirty(true);
|
||||
await handleSave(getValue);
|
||||
},
|
||||
[handleSave]
|
||||
const childRef = React.useRef<HTMLDivElement>(null);
|
||||
const childOffsetHeight = childRef.current?.offsetHeight || 0;
|
||||
const editorStyle = React.useMemo(
|
||||
() => ({
|
||||
padding: "0 32px",
|
||||
margin: "0 -32px",
|
||||
paddingBottom: `calc(50vh - ${childOffsetHeight}px)`,
|
||||
}),
|
||||
[childOffsetHeight]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
setEditing(false);
|
||||
}, [collection.id]);
|
||||
const placeholder = `${t("Add a description")}…`;
|
||||
const key = isEditing || isDirty ? "draft" : collection.updatedAt;
|
||||
|
||||
return (
|
||||
<MaxHeight data-editing={isEditing} data-expanded={isExpanded}>
|
||||
<Input data-editing={isEditing} data-expanded={isExpanded}>
|
||||
<span onClick={can.update ? handleStartEditing : undefined}>
|
||||
{collections.isSaving && <LoadingIndicator />}
|
||||
{collection.hasDescription || isEditing || isDirty ? (
|
||||
<React.Suspense
|
||||
fallback={
|
||||
<Placeholder
|
||||
onClick={() => {
|
||||
//
|
||||
}}
|
||||
>
|
||||
Loading…
|
||||
</Placeholder>
|
||||
}
|
||||
>
|
||||
<Editor
|
||||
key={key}
|
||||
defaultValue={collection.data}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
readOnly={!isEditing}
|
||||
autoFocus={isEditing}
|
||||
onBlur={handleStopEditing}
|
||||
extensions={extensions}
|
||||
maxLength={1000}
|
||||
embedsDisabled
|
||||
canUpdate
|
||||
/>
|
||||
</React.Suspense>
|
||||
) : (
|
||||
can.update && (
|
||||
<Placeholder
|
||||
onClick={() => {
|
||||
//
|
||||
}}
|
||||
>
|
||||
{placeholder}
|
||||
</Placeholder>
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
</Input>
|
||||
{!isEditing && (
|
||||
<Disclosure
|
||||
onClick={handleClickDisclosure}
|
||||
aria-label={isExpanded ? t("Collapse") : t("Expand")}
|
||||
size={30}
|
||||
>
|
||||
<Arrow />
|
||||
</Disclosure>
|
||||
<>
|
||||
{collections.isSaving && <LoadingIndicator />}
|
||||
{(collection.hasDescription || can.update) && (
|
||||
<React.Suspense fallback={<Placeholder>Loading…</Placeholder>}>
|
||||
<Editor
|
||||
defaultValue={collection.data}
|
||||
onChange={handleSave}
|
||||
placeholder={`${t("Add a description")}…`}
|
||||
extensions={extensions}
|
||||
maxLength={1000}
|
||||
canUpdate={can.update}
|
||||
readOnly={!can.update}
|
||||
editorStyle={editorStyle}
|
||||
embedsDisabled
|
||||
/>
|
||||
<div ref={childRef} />
|
||||
</React.Suspense>
|
||||
)}
|
||||
</MaxHeight>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const Disclosure = styled(NudeButton)`
|
||||
opacity: 0;
|
||||
color: ${s("divider")};
|
||||
position: absolute;
|
||||
top: calc(25vh - 50px);
|
||||
left: 50%;
|
||||
z-index: 1;
|
||||
transform: rotate(-90deg) translateX(-50%);
|
||||
transition: opacity 100ms ease-in-out;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: ${s("sidebarText")};
|
||||
}
|
||||
`;
|
||||
|
||||
const Placeholder = styled(ButtonLink)`
|
||||
const Placeholder = styled(Text)`
|
||||
color: ${s("placeholder")};
|
||||
cursor: text;
|
||||
min-height: 27px;
|
||||
`;
|
||||
|
||||
const MaxHeight = styled.div`
|
||||
position: relative;
|
||||
max-height: 25vh;
|
||||
overflow: hidden;
|
||||
margin: 8px -8px -8px;
|
||||
padding: 8px;
|
||||
|
||||
&[data-editing="true"],
|
||||
&[data-expanded="true"] {
|
||||
max-height: initial;
|
||||
overflow: initial;
|
||||
|
||||
${Disclosure} {
|
||||
top: initial;
|
||||
bottom: 0;
|
||||
transform: rotate(90deg) translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover ${Disclosure} {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const Input = styled.div`
|
||||
margin: -8px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: calc(25vh - 50px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 50px;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
${(props) => transparentize(1, props.theme.background)} 0%,
|
||||
${s("background")} 100%
|
||||
);
|
||||
}
|
||||
|
||||
&[data-editing="true"],
|
||||
&[data-expanded="true"] {
|
||||
&:after {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-editing="true"] {
|
||||
background: ${s("backgroundSecondary")};
|
||||
}
|
||||
|
||||
.block-menu-trigger,
|
||||
.heading-anchor {
|
||||
display: none !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export default observer(CollectionDescription);
|
||||
|
||||
@@ -157,6 +157,9 @@ function MentionMenu({ search, isActive, ...rest }: Props) {
|
||||
if (item.attrs.type === MentionType.Document) {
|
||||
return;
|
||||
}
|
||||
if (!documentId) {
|
||||
return;
|
||||
}
|
||||
// Check if the mentioned user has access to the document
|
||||
const res = await client.post("/documents.users", {
|
||||
id: documentId,
|
||||
|
||||
30
app/editor/extensions/index.ts
Normal file
30
app/editor/extensions/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import Extension from "@shared/editor/lib/Extension";
|
||||
import Mark from "@shared/editor/marks/Mark";
|
||||
import Node from "@shared/editor/nodes/Node";
|
||||
import BlockMenuExtension from "~/editor/extensions/BlockMenu";
|
||||
import ClipboardTextSerializer from "~/editor/extensions/ClipboardTextSerializer";
|
||||
import EmojiMenuExtension from "~/editor/extensions/EmojiMenu";
|
||||
import FindAndReplaceExtension from "~/editor/extensions/FindAndReplace";
|
||||
import HoverPreviewsExtension from "~/editor/extensions/HoverPreviews";
|
||||
import Keys from "~/editor/extensions/Keys";
|
||||
import MentionMenuExtension from "~/editor/extensions/MentionMenu";
|
||||
import PasteHandler from "~/editor/extensions/PasteHandler";
|
||||
import PreventTab from "~/editor/extensions/PreventTab";
|
||||
import SmartText from "~/editor/extensions/SmartText";
|
||||
|
||||
type Nodes = (typeof Node | typeof Mark | typeof Extension)[];
|
||||
|
||||
export const withUIExtensions = (nodes: Nodes) => [
|
||||
...nodes,
|
||||
SmartText,
|
||||
PasteHandler,
|
||||
ClipboardTextSerializer,
|
||||
BlockMenuExtension,
|
||||
EmojiMenuExtension,
|
||||
MentionMenuExtension,
|
||||
FindAndReplaceExtension,
|
||||
HoverPreviewsExtension,
|
||||
// Order these default key handlers last
|
||||
PreventTab,
|
||||
Keys,
|
||||
];
|
||||
@@ -72,8 +72,7 @@ function AuthenticatedRoutes() {
|
||||
<Redirect exact from="/templates" to={settingsPath("templates")} />
|
||||
<Redirect exact from="/collections/*" to="/collection/*" />
|
||||
<Route exact path="/collection/:id/new" component={DocumentNew} />
|
||||
<Route exact path="/collection/:id/:tab" component={Collection} />
|
||||
<Route exact path="/collection/:id" component={Collection} />
|
||||
<Route exact path="/collection/:id/:tab?" component={Collection} />
|
||||
<Route exact path="/doc/new" component={DocumentNew} />
|
||||
<Route exact path={`/d/${slug}`} component={RedirectDocument} />
|
||||
<Route
|
||||
|
||||
@@ -3,12 +3,12 @@ import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
useParams,
|
||||
Redirect,
|
||||
Switch,
|
||||
Route,
|
||||
useHistory,
|
||||
useRouteMatch,
|
||||
useLocation,
|
||||
Redirect,
|
||||
} from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
@@ -30,13 +30,13 @@ import PaginatedDocumentList from "~/components/PaginatedDocumentList";
|
||||
import PinnedDocuments from "~/components/PinnedDocuments";
|
||||
import PlaceholderText from "~/components/PlaceholderText";
|
||||
import Scene from "~/components/Scene";
|
||||
import Subheading from "~/components/Subheading";
|
||||
import Tab from "~/components/Tab";
|
||||
import Tabs from "~/components/Tabs";
|
||||
import { editCollection } from "~/actions/definitions/collections";
|
||||
import useCommandBarActions from "~/hooks/useCommandBarActions";
|
||||
import { useLastVisitedPath } from "~/hooks/useLastVisitedPath";
|
||||
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
|
||||
import usePersistedState from "~/hooks/usePersistedState";
|
||||
import { usePinnedDocuments } from "~/hooks/usePinnedDocuments";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import useStores from "~/hooks/useStores";
|
||||
@@ -50,7 +50,16 @@ import ShareButton from "./components/ShareButton";
|
||||
|
||||
const IconPicker = React.lazy(() => import("~/components/IconPicker"));
|
||||
|
||||
function CollectionScene() {
|
||||
enum CollectionPath {
|
||||
Overview = "overview",
|
||||
Recent = "recent",
|
||||
Updated = "updated",
|
||||
Published = "published",
|
||||
Old = "old",
|
||||
Alphabetical = "alphabetical",
|
||||
}
|
||||
|
||||
const CollectionScene = observer(function _CollectionScene() {
|
||||
const params = useParams<{ id?: string }>();
|
||||
const history = useHistory();
|
||||
const match = useRouteMatch();
|
||||
@@ -68,11 +77,19 @@ function CollectionScene() {
|
||||
collections.getByUrl(id) || collections.get(id);
|
||||
const can = usePolicy(collection);
|
||||
const { pins, count } = usePinnedDocuments(id, collection?.id);
|
||||
const [collectionTab, setCollectionTab] = usePersistedState<CollectionPath>(
|
||||
`collection-tab:${collection?.id}`,
|
||||
collection?.hasDescription
|
||||
? CollectionPath.Overview
|
||||
: CollectionPath.Recent,
|
||||
{
|
||||
listen: false,
|
||||
}
|
||||
);
|
||||
|
||||
const handleIconChange = React.useCallback(
|
||||
async (icon: string | null, color: string | null) => {
|
||||
await collection?.save({ icon, color });
|
||||
},
|
||||
(icon: string | null, color: string | null) =>
|
||||
collection?.save({ icon, color }),
|
||||
[collection]
|
||||
);
|
||||
|
||||
@@ -122,6 +139,8 @@ function CollectionScene() {
|
||||
return <Search notFound />;
|
||||
}
|
||||
|
||||
const hasOverview = can.update || collection?.hasDescription;
|
||||
|
||||
const fallbackIcon = collection ? (
|
||||
<Icon
|
||||
value={collection.icon ?? "collection"}
|
||||
@@ -130,11 +149,17 @@ function CollectionScene() {
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const tabProps = (path: CollectionPath) => ({
|
||||
exact: true,
|
||||
onClick: () => setCollectionTab(path),
|
||||
to: {
|
||||
pathname: collectionPath(collection!.path, path),
|
||||
state: { sidebarContext },
|
||||
},
|
||||
});
|
||||
|
||||
return collection ? (
|
||||
<Scene
|
||||
// Forced mount prevents animation of pinned documents when navigating
|
||||
// _between_ collections, speeds up perceived performance.
|
||||
key={collection.id}
|
||||
centered={false}
|
||||
textTitle={collection.name}
|
||||
left={
|
||||
@@ -198,135 +223,156 @@ function CollectionScene() {
|
||||
canUpdate={can.update}
|
||||
placeholderCount={count}
|
||||
/>
|
||||
<CollectionDescription collection={collection} />
|
||||
|
||||
<Documents>
|
||||
{!collection.isArchived && (
|
||||
<Tabs>
|
||||
<Tab
|
||||
<Tabs>
|
||||
{hasOverview && (
|
||||
<Tab {...tabProps(CollectionPath.Overview)}>
|
||||
{t("Overview")}
|
||||
</Tab>
|
||||
)}
|
||||
<Tab {...tabProps(CollectionPath.Recent)}>{t("Documents")}</Tab>
|
||||
{!collection.isArchived && (
|
||||
<>
|
||||
<Tab {...tabProps(CollectionPath.Updated)}>
|
||||
{t("Recently updated")}
|
||||
</Tab>
|
||||
<Tab {...tabProps(CollectionPath.Published)}>
|
||||
{t("Recently published")}
|
||||
</Tab>
|
||||
<Tab {...tabProps(CollectionPath.Old)}>
|
||||
{t("Least recently updated")}
|
||||
</Tab>
|
||||
<Tab {...tabProps(CollectionPath.Alphabetical)}>
|
||||
{t("A–Z")}
|
||||
</Tab>
|
||||
</>
|
||||
)}
|
||||
</Tabs>
|
||||
<Switch>
|
||||
<Route path={collectionPath(collection.path)} exact>
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: collectionPath(collection.path),
|
||||
pathname: collectionPath(collection!.path, collectionTab),
|
||||
state: { sidebarContext },
|
||||
}}
|
||||
exact
|
||||
>
|
||||
{t("Documents")}
|
||||
</Tab>
|
||||
<Tab
|
||||
to={{
|
||||
pathname: collectionPath(collection.path, "updated"),
|
||||
state: { sidebarContext },
|
||||
}}
|
||||
exact
|
||||
>
|
||||
{t("Recently updated")}
|
||||
</Tab>
|
||||
<Tab
|
||||
to={{
|
||||
pathname: collectionPath(collection.path, "published"),
|
||||
state: { sidebarContext },
|
||||
}}
|
||||
exact
|
||||
>
|
||||
{t("Recently published")}
|
||||
</Tab>
|
||||
<Tab
|
||||
to={{
|
||||
pathname: collectionPath(collection.path, "old"),
|
||||
state: { sidebarContext },
|
||||
}}
|
||||
exact
|
||||
>
|
||||
{t("Least recently updated")}
|
||||
</Tab>
|
||||
<Tab
|
||||
to={{
|
||||
pathname: collectionPath(collection.path, "alphabetical"),
|
||||
state: { sidebarContext },
|
||||
}}
|
||||
exact
|
||||
>
|
||||
{t("A–Z")}
|
||||
</Tab>
|
||||
</Tabs>
|
||||
)}
|
||||
{collection.isEmpty ? (
|
||||
<Empty collection={collection} />
|
||||
) : !collection.isArchived ? (
|
||||
<Switch>
|
||||
<Route path={collectionPath(collection.path, "alphabetical")}>
|
||||
<PaginatedDocumentList
|
||||
key="alphabetical"
|
||||
documents={documents.alphabeticalInCollection(
|
||||
collection.id
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path={collectionPath(collection.path, CollectionPath.Overview)}
|
||||
>
|
||||
{hasOverview ? (
|
||||
<CollectionDescription collection={collection} />
|
||||
) : (
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: collectionPath(
|
||||
collection.path,
|
||||
CollectionPath.Recent
|
||||
),
|
||||
state: { sidebarContext },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Route>
|
||||
{collection.isEmpty ? (
|
||||
<Empty collection={collection} />
|
||||
) : !collection.isArchived ? (
|
||||
<>
|
||||
<Route
|
||||
path={collectionPath(
|
||||
collection.path,
|
||||
CollectionPath.Alphabetical
|
||||
)}
|
||||
fetch={documents.fetchAlphabetical}
|
||||
options={{
|
||||
collectionId: collection.id,
|
||||
}}
|
||||
/>
|
||||
</Route>
|
||||
<Route path={collectionPath(collection.path, "old")}>
|
||||
<PaginatedDocumentList
|
||||
key="old"
|
||||
documents={documents.leastRecentlyUpdatedInCollection(
|
||||
collection.id
|
||||
>
|
||||
<PaginatedDocumentList
|
||||
key="alphabetical"
|
||||
documents={documents.alphabeticalInCollection(
|
||||
collection.id
|
||||
)}
|
||||
fetch={documents.fetchAlphabetical}
|
||||
options={{
|
||||
collectionId: collection.id,
|
||||
}}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path={collectionPath(collection.path, CollectionPath.Old)}
|
||||
>
|
||||
<PaginatedDocumentList
|
||||
key="old"
|
||||
documents={documents.leastRecentlyUpdatedInCollection(
|
||||
collection.id
|
||||
)}
|
||||
fetch={documents.fetchLeastRecentlyUpdated}
|
||||
options={{
|
||||
collectionId: collection.id,
|
||||
}}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path={collectionPath(
|
||||
collection.path,
|
||||
CollectionPath.Published
|
||||
)}
|
||||
fetch={documents.fetchLeastRecentlyUpdated}
|
||||
options={{
|
||||
collectionId: collection.id,
|
||||
}}
|
||||
/>
|
||||
</Route>
|
||||
<Route path={collectionPath(collection.path, "recent")}>
|
||||
<Redirect to={collectionPath(collection.path, "published")} />
|
||||
</Route>
|
||||
<Route path={collectionPath(collection.path, "published")}>
|
||||
<PaginatedDocumentList
|
||||
key="published"
|
||||
documents={documents.recentlyPublishedInCollection(
|
||||
collection.id
|
||||
>
|
||||
<PaginatedDocumentList
|
||||
key="published"
|
||||
documents={documents.recentlyPublishedInCollection(
|
||||
collection.id
|
||||
)}
|
||||
fetch={documents.fetchRecentlyPublished}
|
||||
options={{
|
||||
collectionId: collection.id,
|
||||
}}
|
||||
showPublished
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path={collectionPath(
|
||||
collection.path,
|
||||
CollectionPath.Updated
|
||||
)}
|
||||
fetch={documents.fetchRecentlyPublished}
|
||||
options={{
|
||||
collectionId: collection.id,
|
||||
}}
|
||||
showPublished
|
||||
/>
|
||||
</Route>
|
||||
<Route path={collectionPath(collection.path, "updated")}>
|
||||
<PaginatedDocumentList
|
||||
key="updated"
|
||||
documents={documents.recentlyUpdatedInCollection(
|
||||
collection.id
|
||||
>
|
||||
<PaginatedDocumentList
|
||||
key="updated"
|
||||
documents={documents.recentlyUpdatedInCollection(
|
||||
collection.id
|
||||
)}
|
||||
fetch={documents.fetchRecentlyUpdated}
|
||||
options={{
|
||||
collectionId: collection.id,
|
||||
}}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path={collectionPath(
|
||||
collection.path,
|
||||
CollectionPath.Recent
|
||||
)}
|
||||
fetch={documents.fetchRecentlyUpdated}
|
||||
options={{
|
||||
collectionId: collection.id,
|
||||
}}
|
||||
/>
|
||||
</Route>
|
||||
<Route path={collectionPath(collection.path)} exact>
|
||||
<PaginatedDocumentList
|
||||
documents={documents.rootInCollection(collection.id)}
|
||||
fetch={documents.fetchPage}
|
||||
options={{
|
||||
collectionId: collection.id,
|
||||
parentDocumentId: null,
|
||||
sort: collection.sort.field,
|
||||
direction: collection.sort.direction,
|
||||
}}
|
||||
showParentDocuments
|
||||
/>
|
||||
</Route>
|
||||
</Switch>
|
||||
) : (
|
||||
<Switch>
|
||||
<Route path={collectionPath(collection.path)} exact>
|
||||
exact
|
||||
>
|
||||
<PaginatedDocumentList
|
||||
documents={documents.rootInCollection(collection.id)}
|
||||
fetch={documents.fetchPage}
|
||||
options={{
|
||||
collectionId: collection.id,
|
||||
parentDocumentId: null,
|
||||
sort: collection.sort.field,
|
||||
direction: collection.sort.direction,
|
||||
}}
|
||||
showParentDocuments
|
||||
/>
|
||||
</Route>
|
||||
</>
|
||||
) : (
|
||||
<Route
|
||||
path={collectionPath(collection.path, CollectionPath.Recent)}
|
||||
exact
|
||||
>
|
||||
<PaginatedDocumentList
|
||||
documents={documents.archivedInCollection(collection.id)}
|
||||
fetch={documents.fetchPage}
|
||||
heading={<Subheading sticky>{t("Documents")}</Subheading>}
|
||||
options={{
|
||||
collectionId: collection.id,
|
||||
parentDocumentId: null,
|
||||
@@ -337,8 +383,8 @@ function CollectionScene() {
|
||||
showParentDocuments
|
||||
/>
|
||||
</Route>
|
||||
</Switch>
|
||||
)}
|
||||
)}
|
||||
</Switch>
|
||||
</Documents>
|
||||
</CenteredContent>
|
||||
</DropToImport>
|
||||
@@ -351,7 +397,15 @@ function CollectionScene() {
|
||||
<PlaceholderList count={5} />
|
||||
</CenteredContent>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const KeyedCollection = () => {
|
||||
const params = useParams<{ id?: string }>();
|
||||
|
||||
// Forced mount prevents animation of pinned documents when navigating
|
||||
// _between_ collections, speeds up perceived performance.
|
||||
return <CollectionScene key={params.id} />;
|
||||
};
|
||||
|
||||
const Documents = styled.div`
|
||||
position: relative;
|
||||
@@ -369,4 +423,4 @@ const CollectionHeading = styled(Heading)`
|
||||
`}
|
||||
`;
|
||||
|
||||
export default observer(CollectionScene);
|
||||
export default KeyedCollection;
|
||||
|
||||
@@ -13,16 +13,7 @@ import { RefHandle } from "~/components/ContentEditable";
|
||||
import { useDocumentContext } from "~/components/DocumentContext";
|
||||
import Editor, { Props as EditorProps } from "~/components/Editor";
|
||||
import Flex from "~/components/Flex";
|
||||
import BlockMenuExtension from "~/editor/extensions/BlockMenu";
|
||||
import ClipboardTextSerializer from "~/editor/extensions/ClipboardTextSerializer";
|
||||
import EmojiMenuExtension from "~/editor/extensions/EmojiMenu";
|
||||
import FindAndReplaceExtension from "~/editor/extensions/FindAndReplace";
|
||||
import HoverPreviewsExtension from "~/editor/extensions/HoverPreviews";
|
||||
import Keys from "~/editor/extensions/Keys";
|
||||
import MentionMenuExtension from "~/editor/extensions/MentionMenu";
|
||||
import PasteHandler from "~/editor/extensions/PasteHandler";
|
||||
import PreventTab from "~/editor/extensions/PreventTab";
|
||||
import SmartText from "~/editor/extensions/SmartText";
|
||||
import { withUIExtensions } from "~/editor/extensions";
|
||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||
import useCurrentUser from "~/hooks/useCurrentUser";
|
||||
import useFocusedComment from "~/hooks/useFocusedComment";
|
||||
@@ -40,20 +31,7 @@ import MultiplayerEditor from "./AsyncMultiplayerEditor";
|
||||
import DocumentMeta from "./DocumentMeta";
|
||||
import DocumentTitle from "./DocumentTitle";
|
||||
|
||||
const extensions = [
|
||||
...withComments(richExtensions),
|
||||
SmartText,
|
||||
PasteHandler,
|
||||
ClipboardTextSerializer,
|
||||
BlockMenuExtension,
|
||||
EmojiMenuExtension,
|
||||
MentionMenuExtension,
|
||||
FindAndReplaceExtension,
|
||||
HoverPreviewsExtension,
|
||||
// Order these default key handlers last
|
||||
PreventTab,
|
||||
Keys,
|
||||
];
|
||||
const extensions = withUIExtensions(withComments(richExtensions));
|
||||
|
||||
type Props = Omit<EditorProps, "editorStyle"> & {
|
||||
onChangeTitle: (title: string) => void;
|
||||
|
||||
@@ -107,6 +107,7 @@ export const richExtensions: Nodes = [
|
||||
TemplatePlaceholder,
|
||||
Math,
|
||||
MathBlock,
|
||||
Mention,
|
||||
// Container type nodes should be last so that key handlers are registered for content inside
|
||||
// the container nodes first.
|
||||
...listExtensions,
|
||||
@@ -116,4 +117,4 @@ export const richExtensions: Nodes = [
|
||||
/**
|
||||
* Add commenting and mentions to a set of nodes
|
||||
*/
|
||||
export const withComments = (nodes: Nodes) => [...nodes, Mention, Comment];
|
||||
export const withComments = (nodes: Nodes) => [...nodes, Comment];
|
||||
|
||||
@@ -167,8 +167,6 @@
|
||||
"Also, <em>{{collectionName}}</em> is being used as the start view – deleting it will reset the start view to the Home page.": "Also, <em>{{collectionName}}</em> is being used as the start view – deleting it will reset the start view to the Home page.",
|
||||
"Sorry, an error occurred saving the collection": "Sorry, an error occurred saving the collection",
|
||||
"Add a description": "Add a description",
|
||||
"Collapse": "Collapse",
|
||||
"Expand": "Expand",
|
||||
"Type a command or search": "Type a command or search",
|
||||
"Choose a template": "Choose a template",
|
||||
"Are you sure you want to permanently delete this entire comment thread?": "Are you sure you want to permanently delete this entire comment thread?",
|
||||
@@ -371,6 +369,8 @@
|
||||
"You can't reorder documents in an alphabetically sorted collection": "You can't reorder documents in an alphabetically sorted collection",
|
||||
"Empty": "Empty",
|
||||
"Collections": "Collections",
|
||||
"Collapse": "Collapse",
|
||||
"Expand": "Expand",
|
||||
"Document not supported – try Markdown, Plain text, HTML, or Word": "Document not supported – try Markdown, Plain text, HTML, or Word",
|
||||
"Go back": "Go back",
|
||||
"Go forward": "Go forward",
|
||||
@@ -594,6 +594,7 @@
|
||||
"{{ groupsCount }} groups with access_plural": "{{ groupsCount }} groups with access",
|
||||
"Archived by {{userName}}": "Archived by {{userName}}",
|
||||
"Share": "Share",
|
||||
"Overview": "Overview",
|
||||
"Recently updated": "Recently updated",
|
||||
"Recently published": "Recently published",
|
||||
"Least recently updated": "Least recently updated",
|
||||
|
||||
Reference in New Issue
Block a user